petail 0.3.0 → 0.5.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
- checksums.yaml.gz.sig +0 -0
- data/README.adoc +98 -17
- data/lib/petail/payload.rb +14 -8
- data/petail.gemspec +1 -1
- data.tar.gz.sig +0 -0
- metadata +2 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 417bd45430e7d40be79470dcd1a09f6fbab6ca729a893c02568f944b67592cd5
|
4
|
+
data.tar.gz: 328809222adc5db346e50b7b144a98c3fd4c0f6e32fef701ee03f7d65a33b210
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c01be4f0f48a6b5f9b4622b8f133102a8cc0abe073c9176827748b8848f1208c7682590fd52ae0734a696e6cd2fa3d5beb7fc338fc520bc7afaef9cb55eb9a9c
|
7
|
+
data.tar.gz: 1c684b3c8e69e3a9c54b627226e2c3ebdecafe1cf013b70f69ac0c6582a5ff4394e201ece321eee3e1115b7fa8573e7d234edc97e20c5b609a770b05cdcc7fad
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/README.adoc
CHANGED
@@ -86,6 +86,17 @@ payload.to_xml
|
|
86
86
|
|
87
87
|
💡 You can also use `Petail.new` to create instances if you don't like `Petail.[]`, as shown above, but `.[]` is preferred.
|
88
88
|
|
89
|
+
=== Members
|
90
|
+
|
91
|
+
As briefly shown above, the minimum members (attributes) that make up problem details are:
|
92
|
+
|
93
|
+
* `type` (optional): The full (or relative) URI that links to additional documentation. Default: `"about:blank"`.
|
94
|
+
* `status` (optional): The link:https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status[HTTP status code] (or symbol) that must match your HTTP status code. Default: `nil`.
|
95
|
+
* `title` (optional): The HTTP status label that must match your HTTP status code label. Default: HTTP status label (dynamically computed based on code unless overwritten).
|
96
|
+
* `detail` (optional): The human readable reason for the error (should not include debugging information). Default: `nil`.
|
97
|
+
* `instance` (optional): The full (or relative) URI that represents the cause of the error. Default: `nil`.
|
98
|
+
* `extensions` (optional): A free form hash of additional details. Default: `{}`.
|
99
|
+
|
89
100
|
=== Media Types
|
90
101
|
|
91
102
|
For convenience, you can obtain the necessary media types for your HTTP headers as follows:
|
@@ -170,7 +181,7 @@ Both serialization and deserialization of JSON is supported. For example, given
|
|
170
181
|
[source,ruby]
|
171
182
|
----
|
172
183
|
payload = Petail[
|
173
|
-
type: "https://
|
184
|
+
type: "https://demo.io/problem_details/out_of_credit",
|
174
185
|
title: "You do not have enough credit.",
|
175
186
|
status: 403,
|
176
187
|
detail: "Your current balance is 30, but that costs 50.",
|
@@ -187,22 +198,20 @@ This means you can serialize as follows:
|
|
187
198
|
[source,ruby]
|
188
199
|
----
|
189
200
|
payload.to_json
|
190
|
-
# {"type"
|
201
|
+
# "{\"type\":\"https://demo.io/problem_details/out_of_credit\",\"title\":\"You do not have enough credit.\",\"status\":403,\"detail\":\"Your current balance is 30, but that costs 50.\",\"instance\":\"/accounts/1\",\"balance\":30,\"accounts\":[\"/accounts/1\",\"/accounts/10\"]}"
|
191
202
|
|
192
203
|
payload.to_json indent: " ", space: " ", object_nl: "\n", array_nl: "\n"
|
193
204
|
# {
|
194
|
-
# "type": "https://
|
205
|
+
# "type": "https://demo.io/problem_details/out_of_credit",
|
195
206
|
# "title": "You do not have enough credit.",
|
196
207
|
# "status": 403,
|
197
208
|
# "detail": "Your current balance is 30, but that costs 50.",
|
198
209
|
# "instance": "/accounts/1",
|
199
|
-
# "
|
200
|
-
#
|
201
|
-
# "accounts"
|
202
|
-
#
|
203
|
-
#
|
204
|
-
# ]
|
205
|
-
# }
|
210
|
+
# "balance": 30,
|
211
|
+
# "accounts": [
|
212
|
+
# "/accounts/1",
|
213
|
+
# "/accounts/10"
|
214
|
+
# ]
|
206
215
|
# }
|
207
216
|
----
|
208
217
|
|
@@ -212,7 +221,7 @@ You can also deserialize by taking the result of the above and turning the raw J
|
|
212
221
|
|
213
222
|
[source,ruby]
|
214
223
|
----
|
215
|
-
Petail.from_json "{\"type\":\"https://
|
224
|
+
Petail.from_json "{\"type\":\"https://demo.io/problem_details/out_of_credit\",\"title\":\"You do not have enough credit.\",\"status\":403,\"detail\":\"Your current balance is 30, but that costs 50.\",\"instance\":\"/accounts/1\",\"balance\":30,\"accounts\":[\"/accounts/1\",\"/accounts/10\"]}"
|
216
225
|
|
217
226
|
# #<Struct:Petail::Payload:0x00007670
|
218
227
|
# detail = "Your current balance is 30, but that costs 50.",
|
@@ -226,7 +235,7 @@ Petail.from_json "{\"type\":\"https://test.io/problem_details/out_of_credit\",\"
|
|
226
235
|
# instance = "/accounts/1",
|
227
236
|
# status = 403,
|
228
237
|
# title = "You do not have enough credit.",
|
229
|
-
# type = "https://
|
238
|
+
# type = "https://demo.io/problem_details/out_of_credit"
|
230
239
|
# >
|
231
240
|
----
|
232
241
|
|
@@ -237,7 +246,7 @@ XML is supported too but isn't as robust as JSON support, at the moment. This is
|
|
237
246
|
[source,ruby]
|
238
247
|
----
|
239
248
|
payload = Petail[
|
240
|
-
type: "https://
|
249
|
+
type: "https://demo.io/problem_details/out_of_credit",
|
241
250
|
title: "You do not have enough credit.",
|
242
251
|
status: 403,
|
243
252
|
detail: "Your current balance is 30, but that costs 50.",
|
@@ -254,13 +263,13 @@ This means you can serialize as follows:
|
|
254
263
|
[source,ruby]
|
255
264
|
----
|
256
265
|
payload.to_xml
|
257
|
-
# "<?xml version='1.0' encoding='UTF-8'?><problem xmlns='urn:ietf:rfc:7807'><type>https://
|
266
|
+
# "<?xml version='1.0' encoding='UTF-8'?><problem xmlns='urn:ietf:rfc:7807'><type>https://demo.io/problem_details/out_of_credit</type><title>You do not have enough credit.</title><status>403</status><detail>Your current balance is 30, but that costs 50.</detail><instance>/accounts/1</instance><balance>30</balance><accounts><i>/accounts/1</i><i>/accounts/10</i></accounts></problem>"
|
258
267
|
|
259
268
|
payload.to_xml indent: 2
|
260
269
|
# <?xml version='1.0' encoding='UTF-8'?>
|
261
270
|
# <problem xmlns='urn:ietf:rfc:7807'>
|
262
271
|
# <type>
|
263
|
-
# https://
|
272
|
+
# https://demo.io/problem_details/out_of_credit
|
264
273
|
# </type>
|
265
274
|
# <title>
|
266
275
|
# You do not have enough credit.
|
@@ -297,7 +306,7 @@ You can also deserialize by taking the result of the above and turning the raw J
|
|
297
306
|
payload = Petail.from_xml <<~XML
|
298
307
|
<?xml version='1.0' encoding='UTF-8'?>
|
299
308
|
<problem xmlns='urn:ietf:rfc:7807'>
|
300
|
-
<type>https://
|
309
|
+
<type>https://demo.io/problem_details/out_of_credit</type>
|
301
310
|
<title>You do not have enough credit.</title>
|
302
311
|
<status>403</status>
|
303
312
|
<detail>Your current balance is 30, but that costs 50.</detail>
|
@@ -322,10 +331,75 @@ XML
|
|
322
331
|
# instance = "/accounts/1",
|
323
332
|
# status = 403,
|
324
333
|
# title = "You do not have enough credit.",
|
325
|
-
# type = "https://
|
334
|
+
# type = "https://demo.io/problem_details/out_of_credit"
|
326
335
|
# >
|
327
336
|
----
|
328
337
|
|
338
|
+
=== Examples
|
339
|
+
|
340
|
+
There is a lot of useful information you can provide in your problem details depending on the context you are working in. Some have been shown above but here's a few more that might be of interest.
|
341
|
+
|
342
|
+
==== HATEOAS
|
343
|
+
|
344
|
+
With link:https://nordicapis.com/tools-to-make-hateoas-compliance-easier[HATEOAS], you can provide additional information and links for which the client can understand what next actions are available. The below example shows how you can provide additional resources for clients to adjust accordingly:
|
345
|
+
|
346
|
+
[source,ruby]
|
347
|
+
----
|
348
|
+
Petail[
|
349
|
+
type: "https://demo.io/problem_details/rate_limit",
|
350
|
+
title: "Rate limit exceeded",
|
351
|
+
status: 429,
|
352
|
+
detail: "You have exceeded your rate limit of 150 requests per minute",
|
353
|
+
instance: "/articles",
|
354
|
+
extensions: {
|
355
|
+
retry_after: 5,
|
356
|
+
links: [
|
357
|
+
{
|
358
|
+
ref: "self",
|
359
|
+
href: "/articles"
|
360
|
+
},
|
361
|
+
{
|
362
|
+
rel: "retry",
|
363
|
+
href: "/articles",
|
364
|
+
title: "Retry after five minutes"
|
365
|
+
},
|
366
|
+
{
|
367
|
+
rel: "status",
|
368
|
+
href: "/statuses/rate_limit",
|
369
|
+
title: "Check current rate limit usage"
|
370
|
+
}
|
371
|
+
]
|
372
|
+
}
|
373
|
+
]
|
374
|
+
----
|
375
|
+
|
376
|
+
==== Semantic Structure
|
377
|
+
|
378
|
+
In other situations, you might need a different structure in order to aid clients that might be AI driven which needs a semantically structured response in order to course correct. Example:
|
379
|
+
|
380
|
+
[source,ruby]
|
381
|
+
----
|
382
|
+
Petail[
|
383
|
+
type: "https://demo.io/problem_details/invalid_field",
|
384
|
+
title: "Invalid field value",
|
385
|
+
status: 400,
|
386
|
+
detail: "The category requested doesn't exist",
|
387
|
+
instance: "/categories",
|
388
|
+
extensions: {
|
389
|
+
parameters: {
|
390
|
+
category_id: 123
|
391
|
+
},
|
392
|
+
suggestions: [
|
393
|
+
"ruby",
|
394
|
+
"git",
|
395
|
+
"htmx"
|
396
|
+
]
|
397
|
+
}
|
398
|
+
]
|
399
|
+
----
|
400
|
+
|
401
|
+
With the above, the client now knows what parameters where invalid along with relevant suggestions for proceeding. Even better, the suggestions implicitly show the types of IDs that are required.
|
402
|
+
|
329
403
|
== Development
|
330
404
|
|
331
405
|
To contribute, run:
|
@@ -353,6 +427,13 @@ To test, run:
|
|
353
427
|
bin/rake
|
354
428
|
----
|
355
429
|
|
430
|
+
== Resources
|
431
|
+
|
432
|
+
You can find additional resources here:
|
433
|
+
|
434
|
+
* link:https://www.iana.org/assignments/http-problem-types/http-problem-types.xhtml[IANA Hypertext Transfer Protocol (HTTP) Problem Types]: A registered list of problem types you can use.
|
435
|
+
* link:https://github.com/protocol-registries/http-problem-types[HTTP Problem Type Registration Requests]: Where you can register new problem types.
|
436
|
+
|
356
437
|
== link:https://alchemists.io/policies/license[License]
|
357
438
|
|
358
439
|
== link:https://alchemists.io/policies/security[Security]
|
data/lib/petail/payload.rb
CHANGED
@@ -5,8 +5,10 @@ require "rack/utils"
|
|
5
5
|
require "rexml"
|
6
6
|
|
7
7
|
module Petail
|
8
|
+
PRIMARY_KEYS = %i[type title status detail instance].freeze
|
9
|
+
|
8
10
|
# Models the problem details response payload.
|
9
|
-
Payload = Struct.new
|
11
|
+
Payload = Struct.new(*PRIMARY_KEYS, :extensions) do
|
10
12
|
def self.for(**attributes)
|
11
13
|
status = attributes.delete(:status).then { Rack::Utils.status_code it if it }
|
12
14
|
title = attributes.delete(:title).then { it || Rack::Utils::HTTP_STATUS_CODES[status] }
|
@@ -14,18 +16,23 @@ module Petail
|
|
14
16
|
new title:, status:, **attributes
|
15
17
|
end
|
16
18
|
|
17
|
-
def self.from_json
|
19
|
+
def self.from_json body
|
20
|
+
attributes = JSON body, symbolize_names: true
|
21
|
+
extensions = attributes.reject { |key| PRIMARY_KEYS.include? key }
|
22
|
+
|
23
|
+
self.for(**attributes.slice(*PRIMARY_KEYS), extensions:)
|
24
|
+
end
|
18
25
|
|
19
26
|
# :reek:TooManyStatements
|
20
27
|
def self.from_xml body, deserializer: XML::Deserializer
|
21
28
|
elements = REXML::Document.new(body).root.elements
|
22
29
|
|
23
30
|
attributes = elements.each_with_object({extensions: {}}) do |element, collection|
|
24
|
-
name = element.name
|
31
|
+
name = element.name.to_sym
|
25
32
|
text = element.text
|
26
33
|
|
27
34
|
case name
|
28
|
-
when
|
35
|
+
when *PRIMARY_KEYS then collection[name] = text
|
29
36
|
else collection[:extensions].merge! deserializer.call(element)
|
30
37
|
end
|
31
38
|
end
|
@@ -48,11 +55,12 @@ module Petail
|
|
48
55
|
|
49
56
|
def extension?(name) = extensions.key? name
|
50
57
|
|
51
|
-
def to_h =
|
58
|
+
def to_h = {type:, title:, status:, detail:, instance:, **extensions}.compact
|
52
59
|
|
53
60
|
def to_json(*) = to_h.to_json(*)
|
54
61
|
|
55
62
|
# :reek:TooManyStatements
|
63
|
+
# :reek:FeatureEnvy
|
56
64
|
def to_xml(serializer: XML::Serializer, **options)
|
57
65
|
document = REXML::Document.new
|
58
66
|
document.add REXML::XMLDecl.new("1.0", "UTF-8")
|
@@ -60,9 +68,7 @@ module Petail
|
|
60
68
|
problem = REXML::Element.new("problem").add_namespace("urn:ietf:rfc:7807")
|
61
69
|
document.add problem
|
62
70
|
|
63
|
-
|
64
|
-
attributes.merge! attributes.delete :extensions if extensions.any?
|
65
|
-
attributes.each { |name, value| serializer.call name, value, problem }
|
71
|
+
to_h.each { |name, value| serializer.call name, value, problem }
|
66
72
|
|
67
73
|
"".dup.tap { document.write(**options, output: it) }
|
68
74
|
end
|
data/petail.gemspec
CHANGED
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: petail
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brooke Kuhlmann
|
@@ -109,7 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
111
|
requirements: []
|
112
|
-
rubygems_version: 3.
|
112
|
+
rubygems_version: 3.7.1
|
113
113
|
specification_version: 4
|
114
114
|
summary: A RFC 9457 Problem Details for HTTP APIs implementation.
|
115
115
|
test_files: []
|
metadata.gz.sig
CHANGED
Binary file
|