petail 0.2.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f8b4586f6ff3956d439fa35a0aaa64753b3839261a55df4cc9ce83b0bbdd277f
4
- data.tar.gz: 4b20c15bc80355c4d55dd83d365ca3ed8806775385623b005e53aaa61cf9b260
3
+ metadata.gz: 90ccb34a36bfc17201ce4b4881e8f37acbbc3714f076c5de0e7cf1d5ad373346
4
+ data.tar.gz: 0a2ee99b2cd3191aa9ec08473a218436bd259576f3b0e6e31c8779eddcbfe5b0
5
5
  SHA512:
6
- metadata.gz: 1e7a2e97589c26880828fda50953a8cf542b0eb6c1c285bed3e4889d091c8486a5c1e6998e5efae59dc0a5a013e3f30859366d08773155cc3ae96c95beffdbd1
7
- data.tar.gz: 8f69aaadab1e77609b51824528b22e896c6e3c5a7c3b963e9fadd7edb098f232ba6f1cc64f07a3fcf746c0a9e1e16173a5e5bd8dfd7b059fc179e67786f4d608
6
+ metadata.gz: b663a7f90af61259298cd14a8f23c63afbd0d0e6aab80c3c38c0242e7cc240418563021623bcf95e7af88e3368788bdad8ac30178431788f4cc3e552219815b7
7
+ data.tar.gz: 6efc822522e3e828df9c2d81953a8b2a2344e6ec9aec462ebe063cb32feffcc10552b4f874b67888d3e347875a805621df317b429d2a57227ecb55e061530611
checksums.yaml.gz.sig CHANGED
Binary file
data/README.adoc CHANGED
@@ -55,10 +55,12 @@ The quickest way to get started is to create a new instance and then cast as JSO
55
55
 
56
56
  [source,ruby]
57
57
  ----
58
- payload = Petail.new type: "https://demo.io/problem_details/timeout",
59
- status: 413,
60
- detail: "You've exceeded the 5MB upload limit.",
61
- instance: "/profile/3a1bfd54-ae6c-4a61-8d0d-90c132428dc3"
58
+ payload = Petail[
59
+ type: "https://demo.io/problem_details/timeout",
60
+ status: 413,
61
+ detail: "You've exceeded the 5MB upload limit.",
62
+ instance: "/profile/3a1bfd54-ae6c-4a61-8d0d-90c132428dc3"
63
+ ]
62
64
 
63
65
  payload.to_json
64
66
 
@@ -82,6 +84,19 @@ payload.to_xml
82
84
  # </problem>
83
85
  ----
84
86
 
87
+ 💡 You can also use `Petail.new` to create instances if you don't like `Petail.[]`, as shown above, but `.[]` is preferred.
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
+
85
100
  === Media Types
86
101
 
87
102
  For convenience, you can obtain the necessary media types for your HTTP headers as follows:
@@ -97,11 +112,11 @@ Petail.media_type_for :xml # "application/problem+xml"
97
112
 
98
113
  === Payload
99
114
 
100
- You'll always get a `Petail::Payload` object answered back when using `Petail.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:
115
+ You'll always get a `Petail::Payload` object answered back when using `Petail.[]` or `Petail.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:
101
116
 
102
117
  [source,ruby]
103
118
  ----
104
- Petail.new status: 413
119
+ Petail[status: 413]
105
120
  # #<Struct:Petail::Payload:0x0000ec80
106
121
  # detail = nil,
107
122
  # extensions = {},
@@ -116,7 +131,7 @@ Notice that standard HTTP 413 title of "Content Too Large" is provided for you b
116
131
 
117
132
  [source,ruby]
118
133
  ----
119
- Petail.new status: :bad_request
134
+ Petail[status: :bad_request]
120
135
  # #<Struct:Petail::Payload:0x0000f280
121
136
  # detail = nil,
122
137
  # extensions = {},
@@ -133,7 +148,7 @@ Due to the payload being a `Struct`, you have all of the standard methods availa
133
148
 
134
149
  [source,]
135
150
  ----
136
- payload = Petail.new status: :forbidden
151
+ payload = Petail[status: :forbidden]
137
152
 
138
153
  payload.add_extension(:account, "/accounts/1")
139
154
  .add_extension(:balance, 50)
@@ -165,7 +180,7 @@ Both serialization and deserialization of JSON is supported. For example, given
165
180
 
166
181
  [source,ruby]
167
182
  ----
168
- payload = Petail.new(
183
+ payload = Petail[
169
184
  type: "https://test.io/problem_details/out_of_credit",
170
185
  title: "You do not have enough credit.",
171
186
  status: 403,
@@ -175,7 +190,7 @@ payload = Petail.new(
175
190
  balance: 30,
176
191
  accounts: %w[/accounts/1 /accounts/10]
177
192
  }
178
- )
193
+ ]
179
194
  ----
180
195
 
181
196
  This means you can serialize as follows:
@@ -183,7 +198,7 @@ This means you can serialize as follows:
183
198
  [source,ruby]
184
199
  ----
185
200
  payload.to_json
186
- # {"type":"https://test.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","extensions":{"balance":30,"accounts":["/accounts/1","/accounts/10"]}}
201
+ # "{\"type\":\"https://test.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\"]}"
187
202
 
188
203
  payload.to_json indent: " ", space: " ", object_nl: "\n", array_nl: "\n"
189
204
  # {
@@ -192,13 +207,11 @@ payload.to_json indent: " ", space: " ", object_nl: "\n", array_nl: "\n"
192
207
  # "status": 403,
193
208
  # "detail": "Your current balance is 30, but that costs 50.",
194
209
  # "instance": "/accounts/1",
195
- # "extensions": {
196
- # "balance": 30,
197
- # "accounts": [
198
- # "/accounts/1",
199
- # "/accounts/10"
200
- # ]
201
- # }
210
+ # "balance": 30,
211
+ # "accounts": [
212
+ # "/accounts/1",
213
+ # "/accounts/10"
214
+ # ]
202
215
  # }
203
216
  ----
204
217
 
@@ -208,7 +221,7 @@ You can also deserialize by taking the result of the above and turning the raw J
208
221
 
209
222
  [source,ruby]
210
223
  ----
211
- Petail.from_json "{\"type\":\"https://test.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\",\"extensions\":{\"balance\":30,\"accounts\":[\"/accounts/1\",\"/accounts/10\"]}}"
224
+ Petail.from_json "{\"type\":\"https://test.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\"]}"
212
225
 
213
226
  # #<Struct:Petail::Payload:0x00007670
214
227
  # detail = "Your current balance is 30, but that costs 50.",
@@ -232,7 +245,7 @@ XML is supported too but isn't as robust as JSON support, at the moment. This is
232
245
 
233
246
  [source,ruby]
234
247
  ----
235
- payload = Petail.new(
248
+ payload = Petail[
236
249
  type: "https://test.io/problem_details/out_of_credit",
237
250
  title: "You do not have enough credit.",
238
251
  status: 403,
@@ -242,7 +255,7 @@ payload = Petail.new(
242
255
  balance: 30,
243
256
  accounts: %w[/accounts/1 /accounts/10]
244
257
  }
245
- )
258
+ ]
246
259
  ----
247
260
 
248
261
  This means you can serialize as follows:
@@ -349,6 +362,13 @@ To test, run:
349
362
  bin/rake
350
363
  ----
351
364
 
365
+ == Resources
366
+
367
+ You can find additional resources here:
368
+
369
+ * 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.
370
+ * link:https://github.com/protocol-registries/http-problem-types[HTTP Problem Type Registration Requests]: Where you can register new problem types.
371
+
352
372
  == link:https://alchemists.io/policies/license[License]
353
373
 
354
374
  == link:https://alchemists.io/policies/security[Security]
@@ -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 :type, :title, :status, :detail, :instance, :extensions do
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(body) = self.for(**JSON(body, symbolize_names: true))
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 "type", "title", "detail", "instance", "status" then collection[name.to_sym] = text
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 = super.compact.tap { it.delete :extensions if extensions.empty? }
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
- attributes = to_h
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/lib/petail.rb CHANGED
@@ -10,6 +10,10 @@ module Petail
10
10
  MEDIA_TYPE_XML = "application/problem+xml"
11
11
  TYPES = %i[json xml].freeze
12
12
 
13
+ def self.[](**) = Payload.for(**)
14
+
15
+ def self.new(**) = Payload.for(**)
16
+
13
17
  def self.from_json(...) = Payload.from_json(...)
14
18
 
15
19
  def self.from_xml(...) = Payload.from_xml(...)
@@ -17,6 +21,4 @@ module Petail
17
21
  def self.media_type_for key, types: TYPES
18
22
  types.include?(key) ? const_get("MEDIA_TYPE_#{key.upcase}") : ""
19
23
  end
20
-
21
- def self.new(**) = Payload.for(**)
22
24
  end
data/petail.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "petail"
5
- spec.version = "0.2.0"
5
+ spec.version = "0.4.0"
6
6
  spec.authors = ["Brooke Kuhlmann"]
7
7
  spec.email = ["brooke@alchemists.io"]
8
8
  spec.homepage = "https://alchemists.io/projects/petail"
@@ -24,6 +24,7 @@ Gem::Specification.new do |spec|
24
24
 
25
25
  spec.required_ruby_version = "~> 3.4"
26
26
  spec.add_dependency "rack", ">= 2.2", "< 4.0"
27
+ spec.add_dependency "rexml", "~> 3.4"
27
28
 
28
29
  spec.extra_rdoc_files = Dir["README*", "LICENSE*"]
29
30
  spec.files = Dir["*.gemspec", "lib/**/*"]
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.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brooke Kuhlmann
@@ -55,6 +55,20 @@ dependencies:
55
55
  - - "<"
56
56
  - !ruby/object:Gem::Version
57
57
  version: '4.0'
58
+ - !ruby/object:Gem::Dependency
59
+ name: rexml
60
+ requirement: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - "~>"
63
+ - !ruby/object:Gem::Version
64
+ version: '3.4'
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - "~>"
70
+ - !ruby/object:Gem::Version
71
+ version: '3.4'
58
72
  email:
59
73
  - brooke@alchemists.io
60
74
  executables: []
@@ -95,7 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
95
109
  - !ruby/object:Gem::Version
96
110
  version: '0'
97
111
  requirements: []
98
- rubygems_version: 3.6.7
112
+ rubygems_version: 3.6.9
99
113
  specification_version: 4
100
114
  summary: A RFC 9457 Problem Details for HTTP APIs implementation.
101
115
  test_files: []
metadata.gz.sig CHANGED
Binary file