http_resource 0.1.0 → 0.2.0
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 +48 -0
- data/lib/http_resource/client.rb +49 -21
- data/lib/http_resource/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d9fe8b0aeb930b24273f354fa958c855392cbc7169178aff012fc55c9349bc0
|
|
4
|
+
data.tar.gz: c59a5d2b61d6b759f494fc15619b14a26f1040425c6d1d9eb43c90a67218e2d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 882a09948d72787115e9dde41bf305269355f16bc431e929e2b13d7d28372a48fbaa695682b0e73be030412d1e402cde6fe339d946db1991317e3e19be4e16d7
|
|
7
|
+
data.tar.gz: e12dcb9fbb52ea78a319a1fa19c27f906a88c66079e03f0ec3f97dc358e7dafc48772a329509934c24cc9475809d58915d81614c8c06a10e75af886b9a672868
|
data/README.md
CHANGED
|
@@ -54,6 +54,7 @@ client = HttpResource::Client.new(
|
|
|
54
54
|
|
|
55
55
|
client.get(["api", "contacts", id]) # GET, id escaped as ONE path segment
|
|
56
56
|
client.post(["api", "actions"], { foo: 1 }) # POST a JSON body
|
|
57
|
+
client.put(["api", "contacts", id], { name: "Anna" })
|
|
57
58
|
client.patch(["api", "contacts", email], { name: "Anna" })
|
|
58
59
|
client.delete(["api", "contacts", id])
|
|
59
60
|
```
|
|
@@ -65,6 +66,37 @@ Reads return parsed JSON (a `Hash`/`Array`, or `nil` on an empty body). Every
|
|
|
65
66
|
call raises an `HttpResource::ApiError` subclass on a non-2xx response or a
|
|
66
67
|
transport failure.
|
|
67
68
|
|
|
69
|
+
### Form-encoded bodies (OAuth)
|
|
70
|
+
|
|
71
|
+
`post`/`put`/`patch` take a `form:` keyword to send an
|
|
72
|
+
`application/x-www-form-urlencoded` body instead of JSON — for the form-encoded
|
|
73
|
+
endpoints OAuth consumers hit (RFC 6749 token, RFC 7662 introspection, …):
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
token = client.post(["oauth", "token"], form: {
|
|
77
|
+
grant_type: "client_credentials",
|
|
78
|
+
scope: "read write"
|
|
79
|
+
})
|
|
80
|
+
token["access_token"] # a 2xx still returns parsed JSON
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The response side is identical to a JSON call — parsed JSON on a 2xx, a typed
|
|
84
|
+
`ApiError` on a non-2xx — so an OAuth failure is just a rescue-able error whose
|
|
85
|
+
`#body` carries the payload:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
begin
|
|
89
|
+
client.post(["oauth", "token"], form: { grant_type: "authorization_code", code: bad })
|
|
90
|
+
rescue HttpResource::ClientError => e
|
|
91
|
+
e.status # => 400
|
|
92
|
+
JSON.parse(e.body)["error"] # => "invalid_grant"
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Pass **either** a JSON `payload` **or** `form:`, never both (it raises
|
|
97
|
+
`ArgumentError`). Form keys/values are percent-encoded, so untrusted input can't
|
|
98
|
+
inject a header or an extra field.
|
|
99
|
+
|
|
68
100
|
### A process-wide default client
|
|
69
101
|
|
|
70
102
|
```ruby
|
|
@@ -213,6 +245,22 @@ A **`String`** path is the trusted escape hatch and is sent **verbatim** — so
|
|
|
213
245
|
let the framework encode it. The guarantee is covered by a dedicated,
|
|
214
246
|
adversarial spec (`spec/escape_safety_spec.rb`).
|
|
215
247
|
|
|
248
|
+
## Changelog
|
|
249
|
+
|
|
250
|
+
### 0.2.0
|
|
251
|
+
|
|
252
|
+
- Add a `form:` keyword to `post`/`put`/`patch` for `application/x-www-form-urlencoded`
|
|
253
|
+
bodies (OAuth token/introspection and other form-encoded endpoints). Responses
|
|
254
|
+
stay resty: parsed JSON on a 2xx, a typed `ApiError` (with `#body`) on a non-2xx.
|
|
255
|
+
- Add a first-class `put` verb (JSON or `form:` body).
|
|
256
|
+
- Passing both a JSON `payload` and `form:` raises `ArgumentError`; the bodyless
|
|
257
|
+
verbs (`get`/`delete`) reject `form:`.
|
|
258
|
+
|
|
259
|
+
### 0.1.0
|
|
260
|
+
|
|
261
|
+
- Initial release: Net::HTTP transport, typed `ApiError` hierarchy, bang/non-bang
|
|
262
|
+
resources, pluggable auth, per-call timeouts, escape-safe URL building.
|
|
263
|
+
|
|
216
264
|
## Development
|
|
217
265
|
|
|
218
266
|
```sh
|
data/lib/http_resource/client.rb
CHANGED
|
@@ -8,13 +8,14 @@ require "openssl"
|
|
|
8
8
|
|
|
9
9
|
module HttpResource
|
|
10
10
|
# Net::HTTP transport for a single REST host. Resource-oriented: the verbs
|
|
11
|
-
# (get/post/patch/delete) are the primitives a Resource is built on, and
|
|
12
|
-
# an escape hatch for endpoints not yet modelled.
|
|
11
|
+
# (get/post/put/patch/delete) are the primitives a Resource is built on, and
|
|
12
|
+
# also an escape hatch for endpoints not yet modelled.
|
|
13
13
|
#
|
|
14
14
|
# client = HttpResource::Client.new(base_url: "https://api.example.org",
|
|
15
15
|
# auth: HttpResource::Auth.bearer(token))
|
|
16
|
-
# client.get(["api", "contacts", id])
|
|
17
|
-
# client.post(["api", "actions"], { ... })
|
|
16
|
+
# client.get(["api", "contacts", id]) # GET, id escaped as one segment
|
|
17
|
+
# client.post(["api", "actions"], { ... }) # POST a JSON body
|
|
18
|
+
# client.post(["oauth", "token"], form: { ... }) # POST a form body (OAuth, RFC 6749)
|
|
18
19
|
#
|
|
19
20
|
# Reads return parsed JSON (a Hash/Array, or nil on an empty body). Every call
|
|
20
21
|
# raises an HttpResource::ApiError subclass on a non-2xx response or a transport
|
|
@@ -45,31 +46,45 @@ module HttpResource
|
|
|
45
46
|
# an Array of segments (["api", "contacts", email]) each individually escaped.
|
|
46
47
|
# Each verb accepts open_timeout:/read_timeout: to override the client's
|
|
47
48
|
# budget for that one call (e.g. a short read_timeout on a synchronous read).
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
#
|
|
50
|
+
# The body-bearing verbs (post/put/patch) send EITHER a JSON body — the
|
|
51
|
+
# positional `payload` — or a form body — `form: {...}`, encoded as
|
|
52
|
+
# application/x-www-form-urlencoded (for the form-encoded endpoints OAuth
|
|
53
|
+
# consumers hit: RFC 6749 token, RFC 7662 introspection, …). Passing both is
|
|
54
|
+
# a caller bug and raises ArgumentError. The response side is identical either
|
|
55
|
+
# way: parsed JSON on a 2xx, a typed ApiError (with #status + #body) on a
|
|
56
|
+
# non-2xx — so a "400 invalid_grant" is a rescue-able ClientError whose #body
|
|
57
|
+
# carries the error payload.
|
|
58
|
+
def get(path, params: nil, open_timeout: nil, read_timeout: nil)
|
|
59
|
+
request(:get, path, params:, open_timeout:, read_timeout:)
|
|
50
60
|
end
|
|
51
61
|
|
|
52
|
-
def post(path, payload = nil,
|
|
53
|
-
request(:post, path, body: payload,
|
|
62
|
+
def post(path, payload = nil, form: nil, open_timeout: nil, read_timeout: nil)
|
|
63
|
+
request(:post, path, body: payload, form:, open_timeout:, read_timeout:)
|
|
54
64
|
end
|
|
55
65
|
|
|
56
|
-
def
|
|
57
|
-
request(:
|
|
66
|
+
def put(path, payload = nil, form: nil, open_timeout: nil, read_timeout: nil)
|
|
67
|
+
request(:put, path, body: payload, form:, open_timeout:, read_timeout:)
|
|
58
68
|
end
|
|
59
69
|
|
|
60
|
-
def
|
|
61
|
-
request(:
|
|
70
|
+
def patch(path, payload = nil, form: nil, open_timeout: nil, read_timeout: nil)
|
|
71
|
+
request(:patch, path, body: payload, form:, open_timeout:, read_timeout:)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def delete(path, open_timeout: nil, read_timeout: nil)
|
|
75
|
+
request(:delete, path, open_timeout:, read_timeout:)
|
|
62
76
|
end
|
|
63
77
|
|
|
64
78
|
private
|
|
65
79
|
|
|
66
|
-
def request(method, path, body: nil, params: nil, open_timeout: nil, read_timeout: nil)
|
|
80
|
+
def request(method, path, body: nil, form: nil, params: nil, open_timeout: nil, read_timeout: nil)
|
|
67
81
|
# Build the URI + request OUTSIDE the network rescue: a URI::InvalidURIError
|
|
68
|
-
# (bad path)
|
|
69
|
-
# amount)
|
|
70
|
-
#
|
|
82
|
+
# (bad path), JSON::GeneratorError (un-serializable payload, e.g. a NaN
|
|
83
|
+
# amount) or an ArgumentError (both a JSON and a form body) is a
|
|
84
|
+
# deterministic caller bug, and must NOT be masked as a retryable
|
|
85
|
+
# TransportError — that would have a worker retry it forever.
|
|
71
86
|
uri = build_uri(path, params)
|
|
72
|
-
req = build_request(method, uri, body)
|
|
87
|
+
req = build_request(method, uri, body, form)
|
|
73
88
|
connection = http(uri, open_timeout:, read_timeout:)
|
|
74
89
|
begin
|
|
75
90
|
handle(connection.request(req))
|
|
@@ -116,19 +131,32 @@ module HttpResource
|
|
|
116
131
|
ERB::Util.url_encode(str)
|
|
117
132
|
end
|
|
118
133
|
|
|
119
|
-
def build_request(method, uri, body)
|
|
134
|
+
def build_request(method, uri, body, form = nil)
|
|
120
135
|
klass = {
|
|
121
|
-
get: Net::HTTP::Get, post: Net::HTTP::Post,
|
|
136
|
+
get: Net::HTTP::Get, post: Net::HTTP::Post, put: Net::HTTP::Put,
|
|
122
137
|
patch: Net::HTTP::Patch, delete: Net::HTTP::Delete
|
|
123
138
|
}.fetch(method)
|
|
124
139
|
request = klass.new(uri)
|
|
125
140
|
@auth&.apply(request)
|
|
126
141
|
request["Accept"] = "application/json"
|
|
127
|
-
|
|
142
|
+
apply_body(request, body, form)
|
|
143
|
+
request
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# A request carries EITHER a JSON body (`payload`) or a form body (`form:`),
|
|
147
|
+
# never both — passing both is a caller bug. Form values are percent-encoded
|
|
148
|
+
# by URI.encode_www_form (the same encoder used for query params), so an
|
|
149
|
+
# untrusted key/value can't inject a header, a second field, or CRLF.
|
|
150
|
+
def apply_body(request, body, form)
|
|
151
|
+
raise ArgumentError, "pass either a JSON payload or form:, not both" if body && form
|
|
152
|
+
|
|
153
|
+
if form
|
|
154
|
+
request["Content-Type"] = "application/x-www-form-urlencoded"
|
|
155
|
+
request.body = URI.encode_www_form(form)
|
|
156
|
+
elsif body
|
|
128
157
|
request["Content-Type"] = "application/json"
|
|
129
158
|
request.body = JSON.generate(body)
|
|
130
159
|
end
|
|
131
|
-
request
|
|
132
160
|
end
|
|
133
161
|
|
|
134
162
|
def http(uri, open_timeout: nil, read_timeout: nil)
|