problem 0.1.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.
@@ -0,0 +1,90 @@
1
+ # Generated from lib/problem/detailable.rb with RBS::Inline
2
+
3
+ module Problem
4
+ # Declares an exception class as an RFC 9457 problem.
5
+ #
6
+ # class InsufficientBalance < StandardError
7
+ # include Problem::Detailable
8
+ #
9
+ # type "insufficient-balance"
10
+ # status 403
11
+ # title "Insufficient Balance"
12
+ # end
13
+ #
14
+ # raise InsufficientBalance.new(detail: "Required: 100, Available: 50")
15
+ #
16
+ # This holds only what RFC 9457 defines: a type, a title, a status, a per-occurrence
17
+ # detail, and the conversion to Problem::Details. Each is declared literally, which is
18
+ # all a deployment needs to render an error.
19
+ #
20
+ # A deployment that keeps a catalogue of its problems — a registry, an enum, a locale
21
+ # file — derives those three from it instead, by prepending a module to the error
22
+ # class's singleton that overrides `type`, `status` and `title` and calls `super` for a
23
+ # class the catalogue does not cover. Three things make that work and none of them are
24
+ # visible at the call site: ClassMethods is attached with `extend`, so a singleton
25
+ # prepend sits ahead of it; the gem never defines those three on the including class
26
+ # itself; and #to_problem reaches them through `self.class` rather than reading the
27
+ # class attributes behind them.
28
+ #
29
+ # @rbs module-self ::Exception
30
+ # @rbs module-self _DetailableSelf
31
+ module Detailable : ::Exception, _DetailableSelf
32
+ extend ActiveSupport::Concern
33
+
34
+ # The occurrence's own explanation, which RFC 9457 §3.1 defines as specific to this
35
+ # occurrence rather than to the problem type.
36
+ attr_reader detail: String?
37
+
38
+ # Extended into the including class by ActiveSupport::Concern, which picks up a nested
39
+ # ClassMethods by name. Written as a module rather than a `class_methods` block so the
40
+ # body is ordinary code that RBS can describe.
41
+ #
42
+ # @rbs module-self _DetailableClass
43
+ module ClassMethods : _DetailableClass
44
+ # Each reader doubles as its own setter so a subclass can override one declaration
45
+ # and inherit the rest, which is what class_attribute buys over a constant.
46
+ # : (?Integer?) -> Integer?
47
+ def status: (?Integer?) -> Integer?
48
+
49
+ # The identifier this class publishes, resolved against the type prefix when the
50
+ # occurrence is converted.
51
+ # : (?String?) -> String?
52
+ def type: (?String?) -> String?
53
+
54
+ # `interpolations` is part of the signature even though a literal title rarely needs
55
+ # it: a catalogue-backed override reads a template, and this is where the occurrence
56
+ # hands it the values.
57
+ # : (?String?, ?interpolations: Hash[Symbol, untyped]) -> String?
58
+ def title: (?String?, ?interpolations: Hash[Symbol, untyped]) -> String?
59
+
60
+ # Falls back to the global setting, so a deployment declares its authority once.
61
+ # : (?String?) -> String?
62
+ def type_prefix: (?String?) -> String?
63
+ end
64
+
65
+ # Intercepts `detail:` and forwards everything else to the exception, so
66
+ # `raise Klass, "message"` keeps working.
67
+ # : (*untyped, ?detail: String?, **untyped) -> void
68
+ def initialize: (*untyped, ?detail: String?, **untyped) -> void
69
+
70
+ # Values the title template interpolates for this occurrence.
71
+ # : () -> Hash[Symbol, untyped]
72
+ def title_interpolations: () -> Hash[Symbol, untyped]
73
+
74
+ # Extension members this occurrence publishes alongside the RFC 9457 ones.
75
+ # : () -> Hash[Symbol, untyped]
76
+ def problem_extensions: () -> Hash[Symbol, untyped]
77
+
78
+ # A URI identifying this occurrence, which only the caller's request can supply.
79
+ # : () -> String?
80
+ def problem_instance: () -> String?
81
+
82
+ # Response headers this occurrence needs, applied by Problem::Rescuable.
83
+ # : () -> Hash[String, String]
84
+ def problem_headers: () -> Hash[String, String]
85
+
86
+ # This occurrence as an RFC 9457 document.
87
+ # : () -> Details
88
+ def to_problem: () -> Details
89
+ end
90
+ end
@@ -0,0 +1,36 @@
1
+ # Generated from lib/problem/document.rb with RBS::Inline
2
+
3
+ module Problem
4
+ # Serializes an RFC 9457 problem document.
5
+ #
6
+ # Included by Problem::Details, and by any value object that carries the same readers —
7
+ # the way to add typed members is to define a Data over Problem::Details.members rather
8
+ # than to subclass Details, which cannot gain members.
9
+ #
10
+ # @rbs module-self _Document
11
+ module Document : _Document
12
+ # The members RFC 9457 §3.1 defines, in the order they are serialized.
13
+ MEMBERS: Array[Symbol]
14
+
15
+ # The type of a problem that says nothing beyond its HTTP status (RFC 9457 §4.2.1).
16
+ ABOUT_BLANK: String
17
+
18
+ # The media type a problem document is served as (RFC 9457 §3).
19
+ CONTENT_TYPE: String
20
+
21
+ # Extension members are members of the problem object itself, not a nested container,
22
+ # so they merge at the top level. An absent member is omitted rather than sent as null.
23
+ # : () -> Hash[Symbol, untyped]
24
+ def to_h: () -> Hash[Symbol, untyped]
25
+
26
+ # The document with string keys, so a member whose value knows how to render itself
27
+ # gets the chance before JSON.generate sees it.
28
+ # : (?untyped) -> Hash[String, untyped]
29
+ def as_json: (?untyped) -> Hash[String, untyped]
30
+
31
+ # Defined explicitly because the generic Object#to_json would serialize the Data's
32
+ # inspect output, which looks like a response body until someone reads one.
33
+ # : (*untyped) -> String
34
+ def to_json: (*untyped) -> String
35
+ end
36
+ end
@@ -0,0 +1,40 @@
1
+ # Generated from lib/problem/exceptions_app.rb with RBS::Inline
2
+
3
+ module Problem
4
+ # Answers an exception that escaped the controller — a routing error, an unreadable
5
+ # body, a failure in middleware — as application/problem+json. Nothing else sees those:
6
+ # they are raised before dispatch, so no controller rescue_from ever runs.
7
+ #
8
+ # config.exceptions_app = Problem::ExceptionsApp.new(
9
+ # ActionDispatch::PublicExceptions.new(Rails.public_path),
10
+ # )
11
+ #
12
+ # A wrapper rather than an ActionDispatch::PublicExceptions subclass, so each layer of a
13
+ # deployment's stack claims the requests it recognizes and passes on the rest; a
14
+ # subclass can only ever be the innermost one.
15
+ class ExceptionsApp
16
+ # Wraps the exceptions app to fall back to, which keeps whatever it already answers
17
+ # for the requests this one does not claim.
18
+ # : (untyped app) -> void
19
+ def initialize: (untyped app) -> void
20
+
21
+ # Answers with a problem document, or hands the request to the wrapped app.
22
+ # : (Hash[String, untyped]) -> [Integer, Hash[String, String], Array[String]]
23
+ def call: (Hash[String, untyped]) -> [ Integer, Hash[String, String], Array[String] ]
24
+
25
+ # Browser traffic keeps getting the wrapped app's static error pages.
26
+ # : (Hash[String, untyped]) -> bool
27
+ private def html?: (Hash[String, untyped]) -> bool
28
+
29
+ # The problem to send for an escaped exception. The title is the status's own text,
30
+ # never the exception's message, which can quote internals.
31
+ #
32
+ # Override to publish a type: a deployment with a catalogue of problems maps the
33
+ # status onto it here, so every identifier a client can see comes from one place.
34
+ # : (Exception, Hash[String, untyped]) -> Details
35
+ private def problem_for: (Exception, Hash[String, untyped]) -> Details
36
+
37
+ # : (Details) -> [Integer, Hash[String, String], Array[String]]
38
+ private def render_problem: (Details) -> [ Integer, Hash[String, String], Array[String] ]
39
+ end
40
+ end
@@ -0,0 +1,55 @@
1
+ # Generated from lib/problem/i18nable.rb with RBS::Inline
2
+
3
+ module Problem
4
+ # Looks a problem's title up through I18n instead of declaring it in Ruby.
5
+ #
6
+ # class ApiError < StandardError
7
+ # include Problem::I18nable
8
+ # end
9
+ #
10
+ # class NotFound < ApiError
11
+ # type "not-found"
12
+ # status 404
13
+ # end
14
+ #
15
+ # en:
16
+ # problem_details:
17
+ # titles:
18
+ # not_found: "Not Found"
19
+ #
20
+ # Titles are keyed by the declared type, dashes replaced, so two classes publishing one
21
+ # type publish one title as well. That is what keeps a subclass written to be
22
+ # indistinguishable from its parent indistinguishable in the title too.
23
+ #
24
+ # A class that declares a literal title keeps it, so a codebase can move over gradually.
25
+ #
26
+ # i18n is not a declared dependency of this gem. Referencing this module is opting in,
27
+ # and it is autoloaded so that a host which never does pays nothing for it.
28
+ #
29
+ # @rbs module-self ::Exception
30
+ # @rbs module-self _DetailableSelf
31
+ module I18nable : ::Exception, _DetailableSelf
32
+ extend ActiveSupport::Concern
33
+
34
+ include Detailable
35
+
36
+ # Where titles live unless a class says otherwise.
37
+ DEFAULT_SCOPE: String
38
+
39
+ # @rbs module-self _I18nableClass
40
+ module ClassMethods : _I18nableClass
41
+ # The I18n scope titles are looked up under.
42
+ # : (?String?) -> String
43
+ def title_scope: (?String?) -> String
44
+
45
+ # The key within that scope, derived from the declared type unless it is set.
46
+ # : (?(String | Symbol)?) -> String?
47
+ def title_key: (?(String | Symbol)?) -> String?
48
+
49
+ # Falls through to the literal DSL for a class that declares a title of its own, or
50
+ # that has no type to build a key from.
51
+ # : (?String?, ?interpolations: Hash[Symbol, untyped]) -> String?
52
+ def title: (?String?, ?interpolations: Hash[Symbol, untyped]) -> String?
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,12 @@
1
+ # Generated from lib/problem/railtie.rb with RBS::Inline
2
+
3
+ module Problem
4
+ # Wires the gem into a Rails application on boot.
5
+ #
6
+ # config.problem.type_prefix = "https://api-probs.example.com/"
7
+ #
8
+ # config.exceptions_app is deliberately left alone: replacing what an application set
9
+ # there is not an initializer's business, so Problem::ExceptionsApp is wired by hand.
10
+ class Railtie < ::Rails::Railtie
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ # Generated from lib/problem/renderer.rb with RBS::Inline
2
+
3
+ module Problem
4
+ # Registers the application/problem+json media type and the `problem:` renderer.
5
+ #
6
+ # render problem: error.to_problem
7
+ #
8
+ # Installed by Problem::Railtie on boot; a Rack host or a spec that never boots Rails
9
+ # calls Problem.install! itself.
10
+ module Renderer
11
+ # Renders whatever it is handed, so a deployment can pass its own serializer's object
12
+ # rather than a Problem::Details as long as it answers #status and #to_json.
13
+ # : () -> void
14
+ def self.install!: () -> void
15
+ end
16
+ end
@@ -0,0 +1,54 @@
1
+ # Generated from lib/problem/rescuable.rb with RBS::Inline
2
+
3
+ module Problem
4
+ # Renders any Problem::Detailable a controller raises as application/problem+json.
5
+ #
6
+ # class ApplicationController < ActionController::API
7
+ # include Problem::Rescuable
8
+ # end
9
+ #
10
+ # One registration covers every error class that mixed Problem::Detailable in, because
11
+ # rescue_from matches a Module with ===.
12
+ #
13
+ # Each step is its own method, so a deployment overrides the one it needs and a
14
+ # different transport — a Connect RPC error, say — replaces only #render_problem.
15
+ #
16
+ # @rbs module-self _RescuableSelf
17
+ module Rescuable : _RescuableSelf
18
+ extend ActiveSupport::Concern
19
+
20
+ # : (detailable_error) -> void
21
+ private def render_problem_detailable: (detailable_error) -> void
22
+
23
+ # Wraps building, reporting and rendering. Override to re-establish per-request state
24
+ # that has already unwound: ActionController::Rescue wraps
25
+ # AbstractController::Callbacks, so an around_action is gone by the time a rescue_from
26
+ # handler runs, and #problem_for may read a translation.
27
+ #
28
+ # private def around_problem_render(&) = I18n.with_locale(negotiated_locale, &)
29
+ #
30
+ # : () { () -> void } -> void
31
+ private def around_problem_render: () { () -> void } -> void
32
+
33
+ # Override to fill members only the request can supply, such as `instance`.
34
+ # : (detailable_error) -> Details
35
+ private def problem_for: (detailable_error) -> Details
36
+
37
+ # A client error the API expected is not an incident; a server-side failure is.
38
+ # : (detailable_error, Details) -> bool
39
+ private def report_problem?: (detailable_error, Details) -> bool
40
+
41
+ # A rescue_from handler swallows the exception, so nothing logs it and no error
42
+ # reporter sees it. Reported through the registry Rails error reporters subscribe to,
43
+ # rather than to a particular one.
44
+ # : (detailable_error, Details) -> void
45
+ private def report_problem: (detailable_error, Details) -> void
46
+
47
+ # : (detailable_error) -> void
48
+ private def apply_problem_headers: (detailable_error) -> void
49
+
50
+ # The only transport-specific step.
51
+ # : (Details, detailable_error) -> void
52
+ private def render_problem: (Details, detailable_error) -> void
53
+ end
54
+ end
@@ -0,0 +1,47 @@
1
+ # Generated from lib/problem/retry_after.rb with RBS::Inline
2
+
3
+ module Problem
4
+ # Publishes when a rejected request may be retried, as a Retry-After response header
5
+ # (RFC 9110 §10.2.3) and a `retry_after` extension member carrying the same seconds.
6
+ #
7
+ # Mixed in after Problem::Detailable, whose hooks it extends rather than replaces:
8
+ #
9
+ # class Throttled < StandardError
10
+ # include Problem::Detailable
11
+ # include Problem::RetryAfter
12
+ #
13
+ # status 429
14
+ # title "Try again in %{retry_after} seconds"
15
+ # end
16
+ #
17
+ # raise Throttled.new(retry_after: 30)
18
+ #
19
+ # @rbs module-self Detailable
20
+ module RetryAfter : Detailable
21
+ # The response header the wait is published in.
22
+ HEADER: String
23
+
24
+ # Seconds to wait, or the Time to wait until.
25
+ attr_reader retry_after: Integer | Time
26
+
27
+ # Takes the wait as seconds or as the Time to wait until, and forwards the rest.
28
+ # : (*untyped, retry_after: (Integer | Time), **untyped) -> void
29
+ def initialize: (*untyped, retry_after: Integer | Time, **untyped) -> void
30
+
31
+ # A deadline that has already passed is published as 0 rather than as a negative wait.
32
+ # : () -> Integer
33
+ def retry_after_seconds: () -> Integer
34
+
35
+ # Adds the wait, so a title template can name it.
36
+ # : () -> Hash[Symbol, untyped]
37
+ def title_interpolations: () -> Hash[Symbol, untyped]
38
+
39
+ # Publishes the wait in the document as a `retry_after` extension member.
40
+ # : () -> Hash[Symbol, untyped]
41
+ def problem_extensions: () -> Hash[Symbol, untyped]
42
+
43
+ # Publishes the wait as a Retry-After header, which a generic HTTP client obeys.
44
+ # : () -> Hash[String, String]
45
+ def problem_headers: () -> Hash[String, String]
46
+ end
47
+ end
@@ -0,0 +1,6 @@
1
+ # Generated from lib/problem/version.rb with RBS::Inline
2
+
3
+ module Problem
4
+ # The gem's version.
5
+ VERSION: String
6
+ end
@@ -0,0 +1,39 @@
1
+ # Generated from lib/problem.rb with RBS::Inline
2
+
3
+ # RFC 9457 Problem Details for HTTP APIs.
4
+ module Problem
5
+ # Anything a deployment sets once, rather than per error class.
6
+ class Configuration
7
+ # Resolves a declared type slug into the URI a client dispatches on, so an error class
8
+ # names `"bad-request"` instead of repeating the deployment's authority. Left unset,
9
+ # a slug goes out as-is, which RFC 9457 allows: `type` is a URI reference.
10
+ attr_accessor type_prefix: String?
11
+ end
12
+
13
+ # A URI reference carrying a scheme is already absolute (RFC 3986 §3.1).
14
+ ABSOLUTE_URI: Regexp
15
+
16
+ self.@config: Configuration?
17
+
18
+ # The settings in effect, created on first use.
19
+ # : () -> Configuration
20
+ def self.config: () -> Configuration
21
+
22
+ # Yields the settings, for a host that is not a Rails application.
23
+ #
24
+ # Problem.configure { |c| c.type_prefix = "https://api-probs.example.com/" }
25
+ #
26
+ # : () { (Configuration) -> void } -> void
27
+ def self.configure: () { (Configuration) -> void } -> void
28
+
29
+ # Registers the application/problem+json media type and the `problem:` renderer. Called
30
+ # by Problem::Railtie on boot; a Rack host or a spec that never boots Rails calls it.
31
+ # : () -> void
32
+ def self.install!: () -> void
33
+
34
+ # The type URI a declared value publishes. `about:blank` and anything already absolute
35
+ # are returned untouched — prefixing `about:blank` would produce a URI that looks valid
36
+ # and means nothing.
37
+ # : (String?, ?prefix: String?) -> String?
38
+ def self.type_uri: (String?, ?prefix: String?) -> String?
39
+ end
@@ -0,0 +1,5 @@
1
+ # `Data.define` generates a keyword-taking `initialize` on the class it returns, but core
2
+ # RBS declares none, so a subclass overriding `initialize` has nothing to call `super` on.
3
+ class Data
4
+ def initialize: (**untyped) -> void
5
+ end
@@ -0,0 +1,37 @@
1
+ module Problem
2
+ # What `rescue_from Problem::Detailable` actually hands a handler: the exception itself,
3
+ # carrying the concern. A module self-type constrains includers, not the module's own
4
+ # type, so the two have to be spelled out together.
5
+ type detailable_error = ::Exception & Detailable
6
+
7
+ # The class-level state and DSL Problem::Detailable installs. Declared by hand because
8
+ # `class_attribute` defines its accessors at runtime and `class_methods do ... end`
9
+ # builds ClassMethods from a block, neither of which RBS can infer.
10
+ interface _DetailableClass
11
+ def problem_status: () -> Integer?
12
+ def problem_status=: (Integer?) -> void
13
+ def problem_title: () -> String?
14
+ def problem_title=: (String?) -> void
15
+ def problem_uri: () -> String?
16
+ def problem_uri=: (String?) -> void
17
+ def problem_type_prefix: () -> String?
18
+ def problem_type_prefix=: (String?) -> void
19
+
20
+ def status: (?Integer?) -> Integer?
21
+ def title: (?String?, ?interpolations: Hash[Symbol, untyped]) -> String?
22
+ def type: (?String?) -> String?
23
+ def type_prefix: (?String?) -> String?
24
+
25
+ def name: () -> String?
26
+ end
27
+
28
+ # #to_problem reaches the DSL through `self.class`, which is what keeps a prepended
29
+ # catalogue override in the path.
30
+ interface _DetailableSelf
31
+ def class: () -> _DetailableClass
32
+
33
+ # The including exception's own constructor, which #initialize forwards to. Its shape
34
+ # belongs to that class, so it is untyped here rather than pinned to Exception.
35
+ def initialize: (*untyped, **untyped) -> void
36
+ end
37
+ end
@@ -0,0 +1,24 @@
1
+ module Problem
2
+ # Hand-written: rbs-inline cannot describe a `Data.define` assignment, so
3
+ # lib/problem/details.rb carries no `rbs_inline` magic comment.
4
+ class Details < ::Data
5
+ include Document
6
+
7
+ attr_reader type: String?
8
+ attr_reader title: String?
9
+ attr_reader status: Integer
10
+ attr_reader detail: String?
11
+ attr_reader instance: String?
12
+ attr_reader extensions: Hash[Symbol, untyped]
13
+
14
+ def self.new: (status: Integer, ?type: String?, ?title: String?, ?detail: String?,
15
+ ?instance: String?, ?extensions: Hash[untyped, untyped]) -> Details
16
+
17
+ def self.members: () -> Array[Symbol]
18
+
19
+ def initialize: (status: Integer, ?type: String?, ?title: String?, ?detail: String?,
20
+ ?instance: String?, ?extensions: Hash[untyped, untyped]) -> void
21
+
22
+ private def sanitized_extensions: (Hash[untyped, untyped]) -> Hash[Symbol, untyped]
23
+ end
24
+ end
@@ -0,0 +1,12 @@
1
+ module Problem
2
+ # What Problem::Document needs from the value object including it. Declared by hand
3
+ # because a module cannot state the readers of its includers on its own.
4
+ interface _Document
5
+ def type: () -> String?
6
+ def title: () -> String?
7
+ def status: () -> Integer
8
+ def detail: () -> String?
9
+ def instance: () -> String?
10
+ def extensions: () -> Hash[Symbol, untyped]
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ module Problem
2
+ # The class-level state Problem::I18nable adds, plus what it reaches on
3
+ # Problem::Detailable to fall through to.
4
+ interface _I18nableClass
5
+ def problem_title_scope: () -> String?
6
+ def problem_title_scope=: (String?) -> void
7
+ def problem_title_key: () -> String?
8
+ def problem_title_key=: (String?) -> void
9
+
10
+ def problem_title: () -> String?
11
+ def type: (?String?) -> String?
12
+ def title_scope: (?String?) -> String
13
+ def title_key: (?(String | Symbol)?) -> String?
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ # Rails APIs the gem uses that gem_rbs_collection does not declare.
2
+ module Rails
3
+ class Railtie
4
+ def self.initializer: (String, **untyped) ?{ (untyped) -> void } -> void
5
+ end
6
+
7
+ class Application
8
+ class Configuration
9
+ # Set by Problem::Railtie, read by its initializer.
10
+ def problem: () -> untyped
11
+ def problem=: (untyped) -> void
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ module Problem
2
+ # What Problem::Rescuable needs from the controller it is mixed into. Declared by hand
3
+ # for the same reason the sibling gem declares its controller self-type: a mix-in cannot
4
+ # state the API of its includers.
5
+ interface _RescuableSelf
6
+ def render: (**untyped) -> void
7
+ def response: () -> untyped
8
+ end
9
+ end
10
+
11
+ module ActiveSupport
12
+ # Absent from gem_rbs_collection's activesupport signatures.
13
+ def self.error_reporter: () -> ErrorReporter
14
+
15
+ class ErrorReporter
16
+ def report: (Exception, ?handled: bool, ?severity: Symbol, ?context: Hash[Symbol, untyped],
17
+ ?source: String?) -> void
18
+ end
19
+ end