rfc-api-problem 0.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a19ce32dc8342d753c5102259e58df1195f1f233a6a3cae3c83399e8cfa192c0
4
+ data.tar.gz: 84a471cfc3b1c53ea7a58d49f4de6f67878da29f2977823cf597ec32e083d782
5
+ SHA512:
6
+ metadata.gz: 58a62a1bf5bd36959f083f69c652f21a5428c1d99abba82298c2f6413f1bbbac32f8eb712bc5634181be15262f6481d7580f511ba07f69fc0dc0d5d9dc4361fc
7
+ data.tar.gz: 07442fe2e26b1b882b38c6305380f5f4f9544a6cbed601c915d1b1292ef99c632c5e77cfec65640a6e878472b08d23d69546f1641592b2c3c8ea78164d7b329f
checksums.yaml.gz.sig ADDED
Binary file
data/LICENSE.adoc ADDED
@@ -0,0 +1,134 @@
1
+ = Hippocratic License
2
+
3
+ Version: 2.1.0.
4
+
5
+ Purpose. The purpose of this License is for the Licensor named above to
6
+ permit the Licensee (as defined below) broad permission, if consistent
7
+ with Human Rights Laws and Human Rights Principles (as each is defined
8
+ below), to use and work with the Software (as defined below) within the
9
+ full scope of Licensor’s copyright and patent rights, if any, in the
10
+ Software, while ensuring attribution and protecting the Licensor from
11
+ liability.
12
+
13
+ Permission and Conditions. The Licensor grants permission by this
14
+ license ("License"), free of charge, to the extent of Licensor’s
15
+ rights under applicable copyright and patent law, to any person or
16
+ entity (the "Licensee") obtaining a copy of this software and
17
+ associated documentation files (the "Software"), to do everything with
18
+ the Software that would otherwise infringe (i) the Licensor’s copyright
19
+ in the Software or (ii) any patent claims to the Software that the
20
+ Licensor can license or becomes able to license, subject to all of the
21
+ following terms and conditions:
22
+
23
+ * Acceptance. This License is automatically offered to every person and
24
+ entity subject to its terms and conditions. Licensee accepts this
25
+ License and agrees to its terms and conditions by taking any action with
26
+ the Software that, absent this License, would infringe any intellectual
27
+ property right held by Licensor.
28
+ * Notice. Licensee must ensure that everyone who gets a copy of any part
29
+ of this Software from Licensee, with or without changes, also receives
30
+ the License and the above copyright notice (and if included by the
31
+ Licensor, patent, trademark and attribution notice). Licensee must cause
32
+ any modified versions of the Software to carry prominent notices stating
33
+ that Licensee changed the Software. For clarity, although Licensee is
34
+ free to create modifications of the Software and distribute only the
35
+ modified portion created by Licensee with additional or different terms,
36
+ the portion of the Software not modified must be distributed pursuant to
37
+ this License. If anyone notifies Licensee in writing that Licensee has
38
+ not complied with this Notice section, Licensee can keep this License by
39
+ taking all practical steps to comply within 30 days after the notice. If
40
+ Licensee does not do so, Licensee’s License (and all rights licensed
41
+ hereunder) shall end immediately.
42
+ * Compliance with Human Rights Principles and Human Rights Laws.
43
+ [arabic]
44
+ . Human Rights Principles.
45
+ [loweralpha]
46
+ .. Licensee is advised to consult the articles of the United Nations
47
+ Universal Declaration of Human Rights and the United Nations Global
48
+ Compact that define recognized principles of international human rights
49
+ (the "Human Rights Principles"). Licensee shall use the Software in a
50
+ manner consistent with Human Rights Principles.
51
+ .. Unless the Licensor and Licensee agree otherwise, any dispute,
52
+ controversy, or claim arising out of or relating to (i) Section 1(a)
53
+ regarding Human Rights Principles, including the breach of Section 1(a),
54
+ termination of this License for breach of the Human Rights Principles,
55
+ or invalidity of Section 1(a) or (ii) a determination of whether any Law
56
+ is consistent or in conflict with Human Rights Principles pursuant to
57
+ Section 2, below, shall be settled by arbitration in accordance with the
58
+ Hague Rules on Business and Human Rights Arbitration (the "Rules");
59
+ provided, however, that Licensee may elect not to participate in such
60
+ arbitration, in which event this License (and all rights licensed
61
+ hereunder) shall end immediately. The number of arbitrators shall be one
62
+ unless the Rules require otherwise.
63
+ +
64
+ Unless both the Licensor and Licensee agree to the contrary: (1) All
65
+ documents and information concerning the arbitration shall be public and
66
+ may be disclosed by any party; (2) The repository referred to under
67
+ Article 43 of the Rules shall make available to the public in a timely
68
+ manner all documents concerning the arbitration which are communicated
69
+ to it, including all submissions of the parties, all evidence admitted
70
+ into the record of the proceedings, all transcripts or other recordings
71
+ of hearings and all orders, decisions and awards of the arbitral
72
+ tribunal, subject only to the arbitral tribunal’s powers to take such
73
+ measures as may be necessary to safeguard the integrity of the arbitral
74
+ process pursuant to Articles 18, 33, 41 and 42 of the Rules; and (3)
75
+ Article 26(6) of the Rules shall not apply.
76
+ . Human Rights Laws. The Software shall not be used by any person or
77
+ entity for any systems, activities, or other uses that violate any Human
78
+ Rights Laws. "Human Rights Laws" means any applicable laws,
79
+ regulations, or rules (collectively, "Laws") that protect human,
80
+ civil, labor, privacy, political, environmental, security, economic, due
81
+ process, or similar rights; provided, however, that such Laws are
82
+ consistent and not in conflict with Human Rights Principles (a dispute
83
+ over the consistency or a conflict between Laws and Human Rights
84
+ Principles shall be determined by arbitration as stated above). Where
85
+ the Human Rights Laws of more than one jurisdiction are applicable or in
86
+ conflict with respect to the use of the Software, the Human Rights Laws
87
+ that are most protective of the individuals or groups harmed shall
88
+ apply.
89
+ . Indemnity. Licensee shall hold harmless and indemnify Licensor (and
90
+ any other contributor) against all losses, damages, liabilities,
91
+ deficiencies, claims, actions, judgments, settlements, interest, awards,
92
+ penalties, fines, costs, or expenses of whatever kind, including
93
+ Licensor’s reasonable attorneys’ fees, arising out of or relating to
94
+ Licensee’s use of the Software in violation of Human Rights Laws or
95
+ Human Rights Principles.
96
+ * Failure to Comply. Any failure of Licensee to act according to the
97
+ terms and conditions of this License is both a breach of the License and
98
+ an infringement of the intellectual property rights of the Licensor
99
+ (subject to exceptions under Laws, e.g., fair use). In the event of a
100
+ breach or infringement, the terms and conditions of this License may be
101
+ enforced by Licensor under the Laws of any jurisdiction to which
102
+ Licensee is subject. Licensee also agrees that the Licensor may enforce
103
+ the terms and conditions of this License against Licensee through
104
+ specific performance (or similar remedy under Laws) to the extent
105
+ permitted by Laws. For clarity, except in the event of a breach of this
106
+ License, infringement, or as otherwise stated in this License, Licensor
107
+ may not terminate this License with Licensee.
108
+ * Enforceability and Interpretation. If any term or provision of this
109
+ License is determined to be invalid, illegal, or unenforceable by a
110
+ court of competent jurisdiction, then such invalidity, illegality, or
111
+ unenforceability shall not affect any other term or provision of this
112
+ License or invalidate or render unenforceable such term or provision in
113
+ any other jurisdiction; provided, however, subject to a court
114
+ modification pursuant to the immediately following sentence, if any term
115
+ or provision of this License pertaining to Human Rights Laws or Human
116
+ Rights Principles is deemed invalid, illegal, or unenforceable against
117
+ Licensee by a court of competent jurisdiction, all rights in the
118
+ Software granted to Licensee shall be deemed null and void as between
119
+ Licensor and Licensee. Upon a determination that any term or provision
120
+ is invalid, illegal, or unenforceable, to the extent permitted by Laws,
121
+ the court may modify this License to affect the original purpose that
122
+ the Software be used in compliance with Human Rights Principles and
123
+ Human Rights Laws as closely as possible. The language in this License
124
+ shall be interpreted as to its fair meaning and not strictly for or
125
+ against any party.
126
+ * Disclaimer. TO THE FULL EXTENT ALLOWED BY LAW, THIS SOFTWARE COMES
127
+ "AS IS," WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED, AND LICENSOR AND
128
+ ANY OTHER CONTRIBUTOR SHALL NOT BE LIABLE TO ANYONE FOR ANY DAMAGES OR
129
+ OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE
130
+ OR THIS LICENSE, UNDER ANY KIND OF LEGAL CLAIM.
131
+
132
+ This Hippocratic License is an link:https://ethicalsource.dev[Ethical Source license] and is offered
133
+ for use by licensors and licensees at their own risk, on an "AS IS" basis, and with no warranties
134
+ express or implied, to the maximum extent permitted by Laws.
data/README.adoc ADDED
@@ -0,0 +1,461 @@
1
+ :toc: macro
2
+ :toclevels: 5
3
+ :figure-caption!:
4
+
5
+ :data_link: link:https://alchemists.io/articles/ruby_data[Data]
6
+ :rfc_9457_link: link:https://www.rfc-editor.org/rfc/rfc9457[RFC 9457]
7
+
8
+ = RFC API Problem
9
+
10
+ RFC API Problem implements {rfc_9457_link}: Problem Details for HTTP APIs. This allows you to produce HTTP error responses that are structured, machine readable, and consistent.
11
+
12
+ toc::[]
13
+
14
+ == Features
15
+
16
+ * Provides JSON and XML serialization and deserialization.
17
+ * Provides HTTP header and media type support.
18
+
19
+ == Requirements
20
+
21
+ . link:https://www.ruby-lang.org[Ruby].
22
+
23
+ == Setup
24
+
25
+ To install _with_ security, run:
26
+
27
+ [source,bash]
28
+ ----
29
+ # 💡 Skip this line if you already have the public certificate installed.
30
+ gem cert --add <(curl --compressed --location https://alchemists.io/gems.pem)
31
+ gem install rfc-api-problem --trust-policy HighSecurity
32
+ ----
33
+
34
+ To install _without_ security, run:
35
+
36
+ [source,bash]
37
+ ----
38
+ gem install rfc-api-problem
39
+ ----
40
+
41
+ You can also add the gem directly to your project:
42
+
43
+ [source,bash]
44
+ ----
45
+ bundle add rfc-api-problem
46
+ ----
47
+
48
+ Once the gem is installed, you only need to require it:
49
+
50
+ [source,ruby]
51
+ ----
52
+ require "rfc/api/problem"
53
+ ----
54
+
55
+ == Usage
56
+
57
+ The quickest way to get started is to create a new instance and then cast as JSON or XML:
58
+
59
+ [source,ruby]
60
+ ----
61
+ payload = RFC::API::Problem[
62
+ type: "https://demo.io/problem_details/timeout",
63
+ status: 413,
64
+ detail: "You've exceeded the 5MB upload limit.",
65
+ instance: "/profile/3a1bfd54-ae6c-4a61-8d0d-90c132428dc3"
66
+ ]
67
+
68
+ payload.to_json
69
+
70
+ # {
71
+ # "type" => "https://demo.io/problem_details/timeout",
72
+ # "title" => "Content Too Large",
73
+ # "status" => 413,
74
+ # "detail" => "You've exceeded the 5MB upload limit.",
75
+ # "instance" => "/profile/3a1bfd54-ae6c-4a61-8d0d-90c132428dc3"
76
+ # }
77
+
78
+ payload.to_xml
79
+
80
+ # <?xml version='1.0' encoding='UTF-8'?>
81
+ # <problem xmlns='urn:ietf:rfc:7807'>
82
+ # <type>https://demo.io/problem_details/timeout</type>
83
+ # <title>Content Too Large</title>
84
+ # <status>413</status>
85
+ # <detail>You&apos;ve exceeded the 5MB upload limit.</detail>
86
+ # <instance>/profile/3a1bfd54-ae6c-4a61-8d0d-90c132428dc3</instance>
87
+ # </problem>
88
+ ----
89
+
90
+ 💡 You can also use `RFC::API::Problem.new` to create instances if you don't like `RFC::API::Problem.[]`, as shown above, but `.[]` is preferred.
91
+
92
+ === Members
93
+
94
+ As briefly shown above, the minimum members (attributes) that make up problem details are:
95
+
96
+ * `type` (optional): The full (or relative) URI that links to additional documentation. Default: `"about:blank"`.
97
+ * `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`.
98
+ * `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).
99
+ * `detail` (optional): The human readable reason for the error (should not include debugging information). Default: `nil`.
100
+ * `instance` (optional): The full (or relative) URI that represents the cause of the error. Default: `nil`.
101
+ * `extensions` (optional): A free form hash of additional details. Default: `{}`.
102
+
103
+ === Media Types
104
+
105
+ For convenience, you can obtain the necessary media types for your HTTP headers as follows:
106
+
107
+ [source,ruby]
108
+ ----
109
+ RFC::API::Problem::MEDIA_TYPE_JSON # "application/problem+json"
110
+ RFC::API::Problem::MEDIA_TYPE_XML # "application/problem+xml"
111
+
112
+ RFC::API::Problem.media_type_for :json # "application/problem+json"
113
+ RFC::API::Problem.media_type_for :xml # "application/problem+xml"
114
+ ----
115
+
116
+ === Payload
117
+
118
+ You'll always get a `RFC::API::Problem::Payload` object answered back when using `RFC::API::Problem.[]` or `RFC::API::Problem.new` for which you can cast to JSON, XML, and other types. There are few conveniences provided for you when constructing a new payload. For instance, you can also use status to set default title:
119
+
120
+ [source,ruby]
121
+ ----
122
+ RFC::API::Problem[status: 413]
123
+
124
+ # #<data RFC::API::Problem::Payload:0x00002950
125
+ # detail = nil,
126
+ # extensions = {},
127
+ # instance = nil,
128
+ # status = 413,
129
+ # title = "Content Too Large",
130
+ # type = "about:blank"
131
+ # >
132
+ ----
133
+
134
+ Notice that standard HTTP 413 title of "Content Too Large" is provided for you but only if you don't supply a title. This works for symbols too. Example:
135
+
136
+ [source,ruby]
137
+ ----
138
+ RFC::API::Problem[status: :bad_request]
139
+
140
+ # #<data RFC::API::Problem::Payload:0x00002970
141
+ # detail = nil,
142
+ # extensions = {},
143
+ # instance = nil,
144
+ # status = 400,
145
+ # title = "Bad Request",
146
+ # type = "about:blank"
147
+ # >
148
+ ----
149
+
150
+ This is similar to the above, but notice the status is cast to an integer while the title is also populated for you. Using either an integer or symbol for the HTTP status is handy for situations where you don't need a custom title and prefer the default HTTP title.
151
+
152
+ Due to the payload being a {data_link} object, you have all of the standard methods available to you which are frozen by default. That said, you can still add or check for extensions after the fact. Example:
153
+
154
+ [source,]
155
+ ----
156
+ payload = RFC::API::Problem[status: :forbidden]
157
+
158
+ payload.add_extension(:account, "/accounts/1")
159
+ .add_extension(:balance, 50)
160
+
161
+ # #<data RFC::API::Problem::Payload:0x000029a0
162
+ # detail = nil,
163
+ # extensions = {
164
+ # account: "/accounts/1",
165
+ # balance: 50
166
+ # },
167
+ # instance = nil,
168
+ # status = 403,
169
+ # title = "Forbidden",
170
+ # type = "about:blank"
171
+ # >
172
+ ----
173
+
174
+ Given the above, you can also check if an extension exists:
175
+
176
+ [source,ruby]
177
+ ----
178
+ payload.extension? :account # true
179
+ payload.extension? :bogus # false
180
+ ----
181
+
182
+ === JSON
183
+
184
+ Both serialization and deserialization of JSON is supported. For example, given the following payload:
185
+
186
+ [source,ruby]
187
+ ----
188
+ payload = RFC::API::Problem[
189
+ type: "https://demo.io/problem_details/out_of_credit",
190
+ title: "You do not have enough credit.",
191
+ status: 403,
192
+ detail: "Your current balance is 30, but that costs 50.",
193
+ instance: "/accounts/1",
194
+ extensions: {
195
+ balance: 30,
196
+ accounts: %w[/accounts/1 /accounts/10]
197
+ }
198
+ ]
199
+ ----
200
+
201
+ This means you can serialize as follows:
202
+
203
+ [source,ruby]
204
+ ----
205
+ payload.to_json
206
+
207
+ # "{\"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\"]}"
208
+
209
+ payload.to_json indent: " ", space: " ", object_nl: "\n", array_nl: "\n"
210
+
211
+ # {
212
+ # "type": "https://demo.io/problem_details/out_of_credit",
213
+ # "title": "You do not have enough credit.",
214
+ # "status": 403,
215
+ # "detail": "Your current balance is 30, but that costs 50.",
216
+ # "instance": "/accounts/1",
217
+ # "balance": 30,
218
+ # "accounts": [
219
+ # "/accounts/1",
220
+ # "/accounts/10"
221
+ # ]
222
+ # }
223
+ ----
224
+
225
+ 💡 All of the link:https://docs.ruby-lang.org/en/master/JSON.html#module-JSON-label-Output+Options[JSON] output options are available to you when casting to JSON.
226
+
227
+ You can also deserialize by taking the result of the above and turning the raw JSON back into a `RFC::API::Problem::Payload`:
228
+
229
+ [source,ruby]
230
+ ----
231
+ RFC::API::Problem.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\"]}"
232
+
233
+ # #<data RFC::API::Problem::Payload:0x00002d10
234
+ # detail = "Your current balance is 30, but that costs 50.",
235
+ # extensions = {
236
+ # balance: 30,
237
+ # accounts: [
238
+ # "/accounts/1",
239
+ # "/accounts/10"
240
+ # ]
241
+ # },
242
+ # instance = "/accounts/1",
243
+ # status = 403,
244
+ # title = "You do not have enough credit.",
245
+ # type = "https://demo.io/problem_details/out_of_credit"
246
+ # >
247
+ ----
248
+
249
+ === XML
250
+
251
+ XML is supported too but isn't as robust as JSON support, at the moment. This is mostly due to the fact that extensions can be deeply nested so your mileage may vary. For example, given the following payload:
252
+
253
+ [source,ruby]
254
+ ----
255
+ payload = RFC::API::Problem[
256
+ type: "https://demo.io/problem_details/out_of_credit",
257
+ title: "You do not have enough credit.",
258
+ status: 403,
259
+ detail: "Your current balance is 30, but that costs 50.",
260
+ instance: "/accounts/1",
261
+ extensions: {
262
+ balance: 30,
263
+ accounts: %w[/accounts/1 /accounts/10]
264
+ }
265
+ ]
266
+ ----
267
+
268
+ This means you can serialize as follows:
269
+
270
+ [source,ruby]
271
+ ----
272
+ payload.to_xml
273
+ # "<?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>"
274
+
275
+ payload.to_xml indent: 2
276
+ # <?xml version='1.0' encoding='UTF-8'?>
277
+ # <problem xmlns='urn:ietf:rfc:7807'>
278
+ # <type>
279
+ # https://demo.io/problem_details/out_of_credit
280
+ # </type>
281
+ # <title>
282
+ # You do not have enough credit.
283
+ # </title>
284
+ # <status>
285
+ # 403
286
+ # </status>
287
+ # <detail>
288
+ # Your current balance is 30, but that costs 50.
289
+ # </detail>
290
+ # <instance>
291
+ # /accounts/1
292
+ # </instance>
293
+ # <balance>
294
+ # 30
295
+ # </balance>
296
+ # <accounts>
297
+ # <i>
298
+ # /accounts/1
299
+ # </i>
300
+ # <i>
301
+ # /accounts/10
302
+ # </i>
303
+ # </accounts>
304
+ # </problem>
305
+ ----
306
+
307
+ 💡 All of the link:https://ruby.github.io/rexml/REXML/Document.html#method-i-write[REXML::Document.write] output options are available to you when casting to XML.
308
+
309
+ You can also deserialize by taking the result of the above and turning the raw JSON back into a `RFC::API::Problem::Payload`:
310
+
311
+ [source,ruby]
312
+ ----
313
+ payload = RFC::API::Problem.from_xml <<~XML
314
+ <?xml version='1.0' encoding='UTF-8'?>
315
+ <problem xmlns='urn:ietf:rfc:7807'>
316
+ <type>https://demo.io/problem_details/out_of_credit</type>
317
+ <title>You do not have enough credit.</title>
318
+ <status>403</status>
319
+ <detail>Your current balance is 30, but that costs 50.</detail>
320
+ <instance>/accounts/1</instance>
321
+ <balance>30</balance>
322
+ <accounts>
323
+ <i>/accounts/1</i>
324
+ <i>/accounts/10</i>
325
+ </accounts>
326
+ </problem>
327
+ XML
328
+
329
+ # #<data RFC::API::Problem::Payload:0x00000940
330
+ # detail = "Your current balance is 30, but that costs 50.",
331
+ # extensions = {
332
+ # balance: "30",
333
+ # accounts: [
334
+ # "/accounts/1",
335
+ # "/accounts/10"
336
+ # ]
337
+ # },
338
+ # instance = "/accounts/1",
339
+ # status = 403,
340
+ # title = "You do not have enough credit.",
341
+ # type = "https://demo.io/problem_details/out_of_credit"
342
+ # >
343
+ ----
344
+
345
+ === Examples
346
+
347
+ 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.
348
+
349
+ ==== HATEOAS
350
+
351
+ 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:
352
+
353
+ [source,ruby]
354
+ ----
355
+ RFC::API::Problem[
356
+ type: "https://demo.io/problem_details/rate_limit",
357
+ title: "Rate limit exceeded",
358
+ status: 429,
359
+ detail: "You have exceeded your rate limit of 150 requests per minute",
360
+ instance: "/articles",
361
+ extensions: {
362
+ retry_after: 5,
363
+ links: [
364
+ {
365
+ ref: "self",
366
+ href: "/articles"
367
+ },
368
+ {
369
+ rel: "retry",
370
+ href: "/articles",
371
+ title: "Retry after five minutes"
372
+ },
373
+ {
374
+ rel: "status",
375
+ href: "/statuses/rate_limit",
376
+ title: "Check current rate limit usage"
377
+ }
378
+ ]
379
+ }
380
+ ]
381
+ ----
382
+
383
+ ==== Semantic Structure
384
+
385
+ 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:
386
+
387
+ [source,ruby]
388
+ ----
389
+ RFC::API::Problem[
390
+ type: "https://demo.io/problem_details/invalid_field",
391
+ title: "Invalid field value",
392
+ status: 400,
393
+ detail: "The category requested doesn't exist",
394
+ instance: "/categories",
395
+ extensions: {
396
+ parameters: {
397
+ category_id: 123
398
+ },
399
+ suggestions: [
400
+ "ruby",
401
+ "git",
402
+ "htmx"
403
+ ]
404
+ }
405
+ ]
406
+ ----
407
+
408
+ 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.
409
+
410
+ == Development
411
+
412
+ To contribute, run:
413
+
414
+ [source,bash]
415
+ ----
416
+ git clone https://github.com/bkuhlmann/rfc-api-problem
417
+ cd rfc-api-problem
418
+ bin/setup
419
+ ----
420
+
421
+ You can also use the IRB console for direct access to all objects:
422
+
423
+ [source,bash]
424
+ ----
425
+ bin/console
426
+ ----
427
+
428
+ == Tests
429
+
430
+ To test, run:
431
+
432
+ [source,bash]
433
+ ----
434
+ bin/rake
435
+ ----
436
+
437
+ == Resources
438
+
439
+ You can find additional resources here:
440
+
441
+ * 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.
442
+ * link:https://github.com/protocol-registries/http-problem-types[HTTP Problem Type Registration Requests]: Where you can register new problem types.
443
+
444
+ == link:https://alchemists.io/policies/license[License]
445
+
446
+ == link:https://alchemists.io/policies/security[Security]
447
+
448
+ == link:https://alchemists.io/policies/code_of_conduct[Code of Conduct]
449
+
450
+ == link:https://alchemists.io/policies/contributions[Contributions]
451
+
452
+ == link:https://alchemists.io/policies/developer_certificate_of_origin[Developer Certificate of Origin]
453
+
454
+ == link:https://alchemists.io/projects/rfc-api-problem/versions[Versions]
455
+
456
+ == link:https://alchemists.io/community[Community]
457
+
458
+ == Credits
459
+
460
+ * Built with link:https://alchemists.io/projects/gemsmith[Gemsmith].
461
+ * Engineered by link:https://alchemists.io/team/brooke_kuhlmann[Brooke Kuhlmann].
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "rack/utils"
5
+ require "rexml"
6
+
7
+ module RFC
8
+ module API
9
+ module Problem
10
+ PRIMARY_KEYS = %i[title type status detail instance].freeze
11
+
12
+ # Models the problem details response payload.
13
+ Payload = Data.define(*PRIMARY_KEYS, :extensions) do
14
+ def self.for **attributes
15
+ status = attributes.delete(:status).then { Rack::Utils.status_code it if it }
16
+ title = attributes.delete(:title).then { it || Rack::Utils::HTTP_STATUS_CODES[status] }
17
+
18
+ new title:, status:, **attributes
19
+ end
20
+
21
+ def self.from_json body
22
+ attributes = JSON body, symbolize_names: true
23
+ extensions = attributes.reject { |key| PRIMARY_KEYS.include? key }
24
+
25
+ self.for(**attributes.slice(*PRIMARY_KEYS), extensions:)
26
+ end
27
+
28
+ # :reek:TooManyStatements
29
+ def self.from_xml body, deserializer: XML::Deserializer
30
+ elements = REXML::Document.new(body).root.elements
31
+
32
+ attributes = elements.each_with_object({extensions: {}}) do |element, collection|
33
+ name = element.name.to_sym
34
+ text = element.text
35
+
36
+ case name
37
+ when *PRIMARY_KEYS then collection[name] = text
38
+ else collection[:extensions].merge! deserializer.call(element)
39
+ end
40
+ end
41
+
42
+ self.for(**attributes)
43
+ end
44
+
45
+ def initialize title: nil,
46
+ type: nil,
47
+ status: nil,
48
+ detail: nil,
49
+ instance: nil,
50
+ extensions: nil
51
+ super title: title || Rack::Utils::HTTP_STATUS_CODES[status],
52
+ type: type || "about:blank",
53
+ status:,
54
+ detail:,
55
+ instance:,
56
+ extensions: extensions || {}
57
+ end
58
+
59
+ def add_extension name, value
60
+ extensions[name] = value
61
+ self
62
+ end
63
+
64
+ def extension?(name) = extensions.key? name
65
+
66
+ def to_h = {type:, title:, status:, detail:, instance:, **extensions}.compact
67
+
68
+ def to_json(*) = to_h.to_json(*)
69
+
70
+ # :reek:TooManyStatements
71
+ # :reek:FeatureEnvy
72
+ def to_xml serializer: XML::Serializer, **options
73
+ document = REXML::Document.new
74
+ document.add REXML::XMLDecl.new("1.0", "UTF-8")
75
+
76
+ problem = REXML::Element.new("problem").add_namespace("urn:ietf:rfc:7807")
77
+ document.add problem
78
+
79
+ to_h.each { |name, value| serializer.call name, value, problem }
80
+
81
+ "".dup.tap { document.write(**options, output: it) }
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RFC
4
+ module API
5
+ module Problem
6
+ module XML
7
+ # Converts an XML element into a Hash.
8
+ Deserializer = lambda do |node, attributes = {}|
9
+ if node.has_elements?
10
+ node.each_element do |element|
11
+ if element.name == "i"
12
+ attributes[node.name.to_sym] ||= []
13
+ attributes[node.name.to_sym].push element.text
14
+ elsif element.elements.empty?
15
+ attributes[element.name.to_sym] = element.text
16
+ else
17
+ attributes = {element.name.to_sym => Deserializer.call(element, attributes)}
18
+ end
19
+ end
20
+ else
21
+ attributes[node.name.to_sym] = node.text
22
+ end
23
+
24
+ attributes
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RFC
4
+ module API
5
+ module Problem
6
+ module XML
7
+ # Converts name and value into an XML element.
8
+ Serializer = lambda do |name, value, node|
9
+ if value.is_a? Array
10
+ element = node.add_element name.to_s
11
+ value.map { |item| element.add_element("i").add_text item.to_s }
12
+ else
13
+ node.add_element(name.to_s).add_text value.to_s
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rfc/api/problem/payload"
4
+ require "rfc/api/problem/xml/deserializer"
5
+ require "rfc/api/problem/xml/serializer"
6
+
7
+ module RFC
8
+ module API
9
+ # Main namespace.
10
+ module Problem
11
+ MEDIA_TYPE_JSON = "application/problem+json"
12
+ MEDIA_TYPE_XML = "application/problem+xml"
13
+ TYPES = %i[json xml].freeze
14
+
15
+ def self.[](**) = Payload.for(**)
16
+
17
+ def self.new(**) = Payload.for(**)
18
+
19
+ def self.from_json(...) = Payload.from_json(...)
20
+
21
+ def self.from_xml(...) = Payload.from_xml(...)
22
+
23
+ def self.media_type_for key, types: TYPES
24
+ types.include?(key) ? const_get("MEDIA_TYPE_#{key.upcase}") : ""
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "rfc-api-problem"
5
+ spec.version = "0.0.0"
6
+ spec.authors = ["Brooke Kuhlmann"]
7
+ spec.email = ["brooke@alchemists.io"]
8
+ spec.homepage = "https://alchemists.io/projects/rfc-api-problem"
9
+ spec.summary = "A RFC 9457 Problem Details for HTTP APIs implementation."
10
+ spec.license = "Hippocratic-2.1"
11
+
12
+ spec.metadata = {
13
+ "bug_tracker_uri" => "https://github.com/bkuhlmann/rfc-api-problem/issues",
14
+ "changelog_uri" => "https://alchemists.io/projects/rfc-api-problem/versions",
15
+ "homepage_uri" => "https://alchemists.io/projects/rfc-api-problem",
16
+ "funding_uri" => "https://github.com/sponsors/bkuhlmann",
17
+ "label" => "RFC API Problem",
18
+ "rubygems_mfa_required" => "true",
19
+ "source_code_uri" => "https://github.com/bkuhlmann/rfc-api-problem"
20
+ }
21
+
22
+ spec.signing_key = Gem.default_key_path
23
+ spec.cert_chain = [Gem.default_cert_path]
24
+
25
+ spec.required_ruby_version = ">= 4.0"
26
+
27
+ spec.add_dependency "rack", "~> 3.0"
28
+ spec.add_dependency "rexml", "~> 3.4"
29
+
30
+ spec.extra_rdoc_files = Dir["README*", "LICENSE*"]
31
+ spec.files = Dir["*.gemspec", "lib/**/*"]
32
+ end
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rfc-api-problem
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Brooke Kuhlmann
8
+ bindir: bin
9
+ cert_chain:
10
+ - |
11
+ -----BEGIN CERTIFICATE-----
12
+ MIIENjCCAp6gAwIBAgIBAzANBgkqhkiG9w0BAQsFADBBMQ8wDQYDVQQDDAZicm9v
13
+ a2UxGjAYBgoJkiaJk/IsZAEZFgphbGNoZW1pc3RzMRIwEAYKCZImiZPyLGQBGRYC
14
+ aW8wHhcNMjYwMzI1MTI0OTEyWhcNMjcwMzI1MTI0OTEyWjBBMQ8wDQYDVQQDDAZi
15
+ cm9va2UxGjAYBgoJkiaJk/IsZAEZFgphbGNoZW1pc3RzMRIwEAYKCZImiZPyLGQB
16
+ GRYCaW8wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCro8tj5/E1Hg88
17
+ f4qfiwPVd2zJQHvdYt4GHVvuHRRgx4HGhJuNp+4BId08RBn7V6V1MW6MY3kezRBs
18
+ M+7QOQ4b1xNLTvY7FYQB1wGK5a4x7TTokDrPYQxDB2jmsdDYCzVbIMrAvUfcecRi
19
+ khyGZCdByiiCl4fKv77P12tTT+NfsvXkLt/AYCGwjOUyGKTQ01Z6eC09T27GayPH
20
+ QQvIkakyFgcJtzSyGzs8bzK5q9u7wQ12MNTjJoXzW69lqp0oNvDylu81EiSUb5S6
21
+ QzzPxZBiRB1sgtbt1gUbVI262ZDq1gR+HxPFmp+Cgt7ZLIJZAtesQvtcMzseXpfn
22
+ hpmm0Sw22KGhRAy/mqHBRhDl5HqS1SJp2Ko3lcnpXeFResp0HNlt8NSu13vhC08j
23
+ GUHU9MyIXbFOsnp3K3ADrAVjPWop8EZkmUR3MV/CUm00w2cZHCSGiXl1KMpiVKvk
24
+ Ywr1gd2ZME4QLSo+EXUtLxDUa/W3xnBS8dBOuMMz02FPWYr3PN8CAwEAAaM5MDcw
25
+ CQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFAFgmv0tYMZnItuPycSM
26
+ F5wykJEVMA0GCSqGSIb3DQEBCwUAA4IBgQAG+ykjp+DIXSybGEtX+/ve974mYfN6
27
+ 8U7qcVfRM+qDSOZ+97iu30qUTbVAKIHlHCDKRn3SgOffDUB5VU2MsJBh/3TPKWBZ
28
+ anB/uzMcwOfru+qyA3b7ZFqZzRLWmR5FtPObFxc0gYMT3YvLNHk2Nb9Vjq/PoiGG
29
+ e75PXweDOokwDA5m1gMOz1rdp/dlGMXkSFQg94PPVyUKXgO4VzWTgePSDxOIL+v6
30
+ +OWV6AaEH9BaqxnmdA5ubi0L7bhl0gbN92FxpNO3kpTjww8kme856a+wCK3qyM5w
31
+ 7ZLbUexynDN0Au8eSpT2Bf6ztGmB1S9ffzDJsGX1/lkpMIB51e48Xe2+gzzOgemk
32
+ CdZaGupj6WkarnT8kh/cPtyA5ax4rGX6GOS8meGxzkv8Uy0JSEOYAp6wLfIisYZp
33
+ IJBIXIOkwKKJ0eB5YHrUSJxzpP4LlcIg/eTftaXmJdYjy+2VRrCZYDjfguyLmMjR
34
+ KR9w4/Fjvqy87kCHmxMWa6IL2Vzt1Clm2cA=
35
+ -----END CERTIFICATE-----
36
+ date: 1980-01-02 00:00:00.000000000 Z
37
+ dependencies:
38
+ - !ruby/object:Gem::Dependency
39
+ name: rack
40
+ requirement: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - "~>"
43
+ - !ruby/object:Gem::Version
44
+ version: '3.0'
45
+ type: :runtime
46
+ prerelease: false
47
+ version_requirements: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - "~>"
50
+ - !ruby/object:Gem::Version
51
+ version: '3.0'
52
+ - !ruby/object:Gem::Dependency
53
+ name: rexml
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - "~>"
57
+ - !ruby/object:Gem::Version
58
+ version: '3.4'
59
+ type: :runtime
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - "~>"
64
+ - !ruby/object:Gem::Version
65
+ version: '3.4'
66
+ email:
67
+ - brooke@alchemists.io
68
+ executables: []
69
+ extensions: []
70
+ extra_rdoc_files:
71
+ - LICENSE.adoc
72
+ - README.adoc
73
+ files:
74
+ - LICENSE.adoc
75
+ - README.adoc
76
+ - lib/rfc/api/problem.rb
77
+ - lib/rfc/api/problem/payload.rb
78
+ - lib/rfc/api/problem/xml/deserializer.rb
79
+ - lib/rfc/api/problem/xml/serializer.rb
80
+ - rfc-api-problem.gemspec
81
+ homepage: https://alchemists.io/projects/rfc-api-problem
82
+ licenses:
83
+ - Hippocratic-2.1
84
+ metadata:
85
+ bug_tracker_uri: https://github.com/bkuhlmann/rfc-api-problem/issues
86
+ changelog_uri: https://alchemists.io/projects/rfc-api-problem/versions
87
+ homepage_uri: https://alchemists.io/projects/rfc-api-problem
88
+ funding_uri: https://github.com/sponsors/bkuhlmann
89
+ label: RFC API Problem
90
+ rubygems_mfa_required: 'true'
91
+ source_code_uri: https://github.com/bkuhlmann/rfc-api-problem
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '4.0'
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubygems_version: 4.0.19
107
+ specification_version: 4
108
+ summary: A RFC 9457 Problem Details for HTTP APIs implementation.
109
+ test_files: []
metadata.gz.sig ADDED
Binary file