protobufable 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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +30 -0
  3. data/DESIGN.md +298 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +229 -0
  6. data/docs/columns.md +34 -0
  7. data/docs/connect.md +55 -0
  8. data/docs/errors.md +190 -0
  9. data/docs/request-bodies.md +42 -0
  10. data/docs/responses.md +49 -0
  11. data/docs/validation.md +121 -0
  12. data/lib/protobufable/alba_binding.rb +149 -0
  13. data/lib/protobufable/bad_request.rb +60 -0
  14. data/lib/protobufable/column_validatable.rb +56 -0
  15. data/lib/protobufable/connect/problem_rescuable.rb +92 -0
  16. data/lib/protobufable/connect.rb +20 -0
  17. data/lib/protobufable/field_path.rb +65 -0
  18. data/lib/protobufable/json_type.rb +114 -0
  19. data/lib/protobufable/problem/information.rb +75 -0
  20. data/lib/protobufable/problem/retry_info.rb +62 -0
  21. data/lib/protobufable/problem/typeable.rb +259 -0
  22. data/lib/protobufable/problem.rb +27 -0
  23. data/lib/protobufable/problem_details_pb.rb +17 -0
  24. data/lib/protobufable/problem_pb.rb +15 -0
  25. data/lib/protobufable/protovalidate.rb +15 -0
  26. data/lib/protobufable/railtie.rb +26 -0
  27. data/lib/protobufable/renderers.rb +63 -0
  28. data/lib/protobufable/request_parseable.rb +188 -0
  29. data/lib/protobufable/request_validatable.rb +111 -0
  30. data/lib/protobufable/version.rb +10 -0
  31. data/lib/protobufable.rb +40 -0
  32. data/lib/tapioca/dsl/compilers/protobufable_request_parseable.rb +69 -0
  33. data/proto/protobufable/problem.proto +36 -0
  34. data/proto/protobufable/problem_details.proto +31 -0
  35. data/sig/generated/protobufable/alba_binding.rbs +101 -0
  36. data/sig/generated/protobufable/bad_request.rbs +37 -0
  37. data/sig/generated/protobufable/column_validatable.rbs +23 -0
  38. data/sig/generated/protobufable/connect/problem_rescuable.rbs +49 -0
  39. data/sig/generated/protobufable/connect.rbs +7 -0
  40. data/sig/generated/protobufable/field_path.rbs +33 -0
  41. data/sig/generated/protobufable/json_type.rbs +72 -0
  42. data/sig/generated/protobufable/problem/information.rbs +58 -0
  43. data/sig/generated/protobufable/problem/retry_info.rbs +44 -0
  44. data/sig/generated/protobufable/problem/typeable.rbs +166 -0
  45. data/sig/generated/protobufable/problem.rbs +8 -0
  46. data/sig/generated/protobufable/protovalidate.rbs +2 -0
  47. data/sig/generated/protobufable/railtie.rbs +8 -0
  48. data/sig/generated/protobufable/renderers.rbs +22 -0
  49. data/sig/generated/protobufable/request_parseable.rbs +131 -0
  50. data/sig/generated/protobufable/request_validatable.rbs +71 -0
  51. data/sig/generated/protobufable/version.rbs +6 -0
  52. data/sig/generated/protobufable.rbs +24 -0
  53. data/sig/manual/alba_binding.rbs +29 -0
  54. data/sig/manual/connect/problem_rescuable.rbs +13 -0
  55. data/sig/manual/connect_rpc_rails.rbs +9 -0
  56. data/sig/manual/problem/typeable.rbs +28 -0
  57. data/sig/manual/problem_details_pb.rbs +12 -0
  58. data/sig/manual/problem_pb.rbs +8 -0
  59. data/sig/manual/protovalidate.rbs +38 -0
  60. data/sig/manual/request_parseable.rbs +17 -0
  61. metadata +316 -0
@@ -0,0 +1,101 @@
1
+ # Generated from lib/protobufable/alba_binding.rb with RBS::Inline
2
+
3
+ module Protobufable
4
+ # Binds an Alba resource to the protobuf message it produces, and checks at load time that
5
+ # the two agree on every field.
6
+ #
7
+ # Serialization stays idiomatic — one resource per model, with Alba's association DSL — while
8
+ # the wire format belongs to the schema. The parity check is the point: without it a field
9
+ # added to the .proto has no producer and simply comes back empty, which nothing reports.
10
+ #
11
+ # @example
12
+ # class WidgetResource < ApplicationResource
13
+ # with_protobuf Api::Widget do
14
+ # attributes :id, :name
15
+ # attribute(:created_at) { |w| w.created_at.to_time }
16
+ # end
17
+ # end
18
+ #
19
+ # render proto_json: Api::GetWidgetResponse.new(widget: WidgetResource.new(widget).as_protobuf)
20
+ #
21
+ # The check reads Alba's `@_attributes` and `@_traits`, which are internal to Alba, so an
22
+ # Alba release can break it. A spec pins the assumption against the installed version.
23
+ #
24
+ # @rbs module-self _AlbaBindingSelf
25
+ module AlbaBinding : _AlbaBindingSelf
26
+ # @param base [Class] the Alba resource class including this
27
+ # @return [void]
28
+ # : (untyped base) -> void
29
+ def self.included: (untyped base) -> void
30
+
31
+ # Each of these reaches the binding through `self.class`: Alba installs HelperMethods on
32
+ # instances too, so a bare `message_class` here would resolve to the class-level reader with
33
+ # `self` being the instance, whose ivar is never set.
34
+ #
35
+ # @return [Object] the bound message, built from the serialized attributes
36
+ # : () -> untyped
37
+ def as_protobuf: () -> untyped
38
+
39
+ # @return [String] the bound message encoded as binary protobuf
40
+ # : () -> String
41
+ def to_protobuf: () -> String
42
+
43
+ # Renders through the message's own encoder rather than Alba's, so the wire format is the
44
+ # schema's. Field names and default omission match the proto_json renderer.
45
+ #
46
+ # @return [String] the bound message as ProtoJSON
47
+ # : (*untyped) -> String
48
+ def to_json: (*untyped) -> String
49
+
50
+ # Class-level DSL, installed through Alba's own helper mechanism.
51
+ #
52
+ # @rbs module-self _AlbaBindingClass
53
+ module HelperMethods : _AlbaBindingClass
54
+ # @return [Class] the message class this resource is bound to
55
+ # @raise [Protobufable::Error] when the resource declared no binding
56
+ # : () -> Google::Protobuf::_MessageClass
57
+ def message_class: () -> Google::Protobuf::_MessageClass
58
+
59
+ # Binds this resource to a message and declares its attributes.
60
+ #
61
+ # Both directions are checked: an attribute the message does not declare, and a field no
62
+ # attribute produces, each fail here rather than at response time. The check runs when this
63
+ # does, so an attribute declared after it is never compared; everything belongs inside the
64
+ # block.
65
+ #
66
+ # @param klass [Class] the generated message class
67
+ # @yield the resource's attribute declarations
68
+ # @return [void]
69
+ # @raise [Protobufable::Error] when the two have drifted apart
70
+ # : (Google::Protobuf::_MessageClass klass) { () -> void } -> void
71
+ def with_protobuf: (Google::Protobuf::_MessageClass klass) { () -> void } -> void
72
+ end
73
+
74
+ # Registers a `:timestamp` Alba type that converts to Time.
75
+ #
76
+ # A `google.protobuf.Timestamp` field accepts a Time but refuses an
77
+ # ActiveSupport::TimeWithZone, which is what an ActiveRecord timestamp column hands back,
78
+ # so a resource serializing one needs the conversion.
79
+ #
80
+ # Protobufable::AlbaBinding.register_timestamp_type!
81
+ #
82
+ # attribute :created_at, :timestamp
83
+ #
84
+ # Opt-in rather than done on require: Alba's type registry is global, and a gem should not
85
+ # add to it behind a host's back.
86
+ #
87
+ # @return [void]
88
+ # : () -> void
89
+ def self.register_timestamp_type!: () -> void
90
+
91
+ # Every attribute a resource can produce, including the ones a trait adds.
92
+ #
93
+ # A trait is a block rather than a list, so the only way to see what it declares is to
94
+ # evaluate it, which is what the throwaway subclass is for.
95
+ #
96
+ # @param resource [Class] the Alba resource
97
+ # @return [Array<Symbol>]
98
+ # : (untyped resource) -> Array[Symbol]
99
+ def self.produced_attributes: (untyped resource) -> Array[Symbol]
100
+ end
101
+ end
@@ -0,0 +1,37 @@
1
+ # Generated from lib/protobufable/bad_request.rb with RBS::Inline
2
+
3
+ module Protobufable
4
+ # Turns protovalidate violations into a `google.rpc.BadRequest`, the form AIP-193 defines for
5
+ # telling a client which fields it got wrong.
6
+ #
7
+ # Hand the result to {Problem::Information}, which publishes it in the document.
8
+ #
9
+ # @example
10
+ # class InvalidMessage < Errors::BadRequest
11
+ # include Protobufable::Problem::Information
12
+ #
13
+ # def initialize(violations:, **kwargs)
14
+ # @violations = violations
15
+ # super(**kwargs)
16
+ # end
17
+ #
18
+ # def problem_information
19
+ # super + [Protobufable::BadRequest.for(@violations, message_class)]
20
+ # end
21
+ # end
22
+ #
23
+ # `google.rpc` descriptors are not shipped with this gem; the app that generates googleapis
24
+ # alongside its own protos supplies them.
25
+ module BadRequest
26
+ # The message this builds.
27
+ MESSAGE: String
28
+
29
+ # @param violations [Array<Protovalidate::Violation>]
30
+ # @param message_class [Class] the message the violations were produced from
31
+ # @param json_names [Boolean] whether field paths name fields as ProtoJSON spells them
32
+ # @return [Object] a `google.rpc.BadRequest`, one FieldViolation per violation
33
+ # @raise [Protobufable::Error] when googleapis has not been generated into the pool
34
+ # : (Array[untyped] violations, Google::Protobuf::_MessageClass message_class, ?json_names: bool) -> untyped
35
+ def self.for: (Array[untyped] violations, Google::Protobuf::_MessageClass message_class, ?json_names: bool) -> untyped
36
+ end
37
+ end
@@ -0,0 +1,23 @@
1
+ # Generated from lib/protobufable/column_validatable.rb with RBS::Inline
2
+
3
+ module Protobufable
4
+ # Runs the `buf.validate` rules of every {JsonType} column before the record is saved, so a
5
+ # column holding a message enforces what its .proto declares wherever the record is written:
6
+ # application code, a seed, the console.
7
+ #
8
+ # @example
9
+ # class ApplicationRecord < ActiveRecord::Base
10
+ # include Protobufable::ColumnValidatable
11
+ # end
12
+ #
13
+ # @rbs module-self _ColumnValidatableSelf
14
+ module ColumnValidatable : _ColumnValidatableSelf
15
+ extend ActiveSupport::Concern
16
+
17
+ # Adds one error per violation, under the attribute that holds the message.
18
+ #
19
+ # @return [void]
20
+ # : () -> void
21
+ private def validate_protobuf_message_columns: () -> void
22
+ end
23
+ end
@@ -0,0 +1,49 @@
1
+ # Generated from lib/protobufable/connect/problem_rescuable.rb with RBS::Inline
2
+
3
+ module Protobufable
4
+ module Connect
5
+ # Answers a ::Problem::Detailable raised in a Connect RPC as a Connect error rather than as
6
+ # application/problem+json.
7
+ #
8
+ # The code is read off the problem's status and the message is its detail, or its title when
9
+ # there is none. The document itself travels as a detail, so a caller dispatches on the same
10
+ # `type` over Connect as over REST.
11
+ #
12
+ # @example
13
+ # class WidgetsController < ActionController::API
14
+ # include ConnectRpcRails::Controller
15
+ # include Protobufable::Connect::ProblemRescuable
16
+ #
17
+ # connect_service "myapp.api.WidgetsService"
18
+ # end
19
+ #
20
+ # Include it after ConnectRpcRails::Controller. Only a problem raised inside the controller is
21
+ # answered this way; one escaping before dispatch reaches the exceptions app.
22
+ #
23
+ # @rbs module-self _ProblemRescuableSelf
24
+ module ProblemRescuable : _ProblemRescuableSelf
25
+ extend ActiveSupport::Concern
26
+
27
+ include ::Problem::Rescuable
28
+
29
+ # Builds the message the document travels as. Override to build the application's own copy
30
+ # of the message instead.
31
+ #
32
+ # `information` carries the error's {Problem::Information} messages, packed as they are rather
33
+ # than re-read from the rendered document. Other extension members are dropped, because the
34
+ # message has no slot for them.
35
+ #
36
+ # @param problem [::Problem::Details]
37
+ # @param error [Exception] the error the document was built from
38
+ # @return [Object] a protobuf message
39
+ # : (::Problem::Details problem, untyped error) -> untyped
40
+ private def problem_message_for: (::Problem::Details problem, untyped error) -> untyped
41
+
42
+ # @param problem [::Problem::Details]
43
+ # @param error [Exception]
44
+ # @return [void]
45
+ # : (::Problem::Details problem, untyped error) -> void
46
+ private def render_problem: (::Problem::Details problem, untyped error) -> void
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,7 @@
1
+ # Generated from lib/protobufable/connect.rb with RBS::Inline
2
+
3
+ module Protobufable
4
+ # The connect_rpc_rails integration: problems answered as Connect errors.
5
+ module Connect
6
+ end
7
+ end
@@ -0,0 +1,33 @@
1
+ # Generated from lib/protobufable/field_path.rb with RBS::Inline
2
+
3
+ module Protobufable
4
+ # Renders a `buf.validate.FieldPath` in the spelling the caller sent.
5
+ #
6
+ # A field path is only useful if the client can find the field it names, and a protobuf field
7
+ # has two published names: `preferred_language` in the binary encoding and in ProtoJSON with
8
+ # `preserve_proto_fieldnames`, `preferredLanguage` in ProtoJSON without it. Which one to use
9
+ # is a property of the encoding in play, so each segment is resolved against the descriptor
10
+ # rather than interpolated from the name the violation happened to carry.
11
+ #
12
+ # @example
13
+ # Protobufable::FieldPath.render(violation.field, CreateWidgetRequest, json_names: true)
14
+ # #=> "widget.tags[0]"
15
+ module FieldPath
16
+ # Renders a path against the message it was produced from.
17
+ #
18
+ # @param path [Object, nil] a `buf.validate.FieldPath`
19
+ # @param message_class [Class] the message the path is rooted in
20
+ # @param json_names [Boolean] whether to publish `json_name` rather than the proto name
21
+ # @return [String] the path, empty for a message-level rule
22
+ # : (untyped path, Google::Protobuf::_MessageClass message_class, ?json_names: bool) -> String
23
+ def self.render: (untyped path, Google::Protobuf::_MessageClass message_class, ?json_names: bool) -> String
24
+
25
+ # @return [String] the name this segment is published under
26
+ # : (untyped field, untyped element, json_names: bool) -> String
27
+ def self.publish: (untyped field, untyped element, json_names: bool) -> String
28
+
29
+ # @return [String] the `[...]` suffix for a repeated or map element, empty otherwise
30
+ # : (untyped element) -> String
31
+ def self.subscript: (untyped element) -> String
32
+ end
33
+ end
@@ -0,0 +1,72 @@
1
+ # Generated from lib/protobufable/json_type.rb with RBS::Inline
2
+
3
+ module Protobufable
4
+ # An ActiveModel type storing a protobuf message in a native `json` or `jsonb` column.
5
+ #
6
+ # Encoding goes through the message's own `encode_json` and `decode_json`, so the stored
7
+ # document is constrained to the protobuf schema rather than to whatever a Hash happened to
8
+ # hold. Being a type rather than a `serialize` coder is what buys dirty tracking, casting
9
+ # from a Hash, and a column the adapter knows is JSON.
10
+ #
11
+ # @example A column that is never null in Ruby
12
+ # class Widget < ApplicationRecord
13
+ # attribute :settings, Protobufable::JsonType.new(Internal::WidgetSettings),
14
+ # default: -> { Internal::WidgetSettings.new }
15
+ # end
16
+ class JsonType < ActiveModel::Type::Value
17
+ # @return [Class] the generated message class this column holds
18
+ attr_reader message_class: Google::Protobuf::_MessageClass
19
+
20
+ # @param message_class [Class] the generated message class this column holds
21
+ # @param nil_as_default [Boolean] whether a null column reads back as an empty message,
22
+ # for a nullable column whose Ruby value is semantically non-null
23
+ # @param preserve_proto_fieldnames [Boolean] whether documents are written with the
24
+ # `.proto` field names (`snake_case`) rather than their lowerCamelCase JSON names
25
+ # @return [void]
26
+ # : (Google::Protobuf::_MessageClass message_class, ?nil_as_default: bool, ?preserve_proto_fieldnames: bool) -> void
27
+ def initialize: (Google::Protobuf::_MessageClass message_class, ?nil_as_default: bool, ?preserve_proto_fieldnames: bool) -> void
28
+
29
+ # @return [Symbol] always `:json`, so Rails and the adapter treat the column as JSON
30
+ # : () -> Symbol
31
+ def type: () -> Symbol
32
+
33
+ # Decodes a stored document into a message.
34
+ #
35
+ # Unknown fields are tolerated: during a rolling deploy a row written by a newer schema has
36
+ # to stay readable on a node that has not picked it up yet.
37
+ #
38
+ # @param value [String, nil] the document as the adapter hands it back
39
+ # @return [Object, nil] the decoded message, or nil for a null column
40
+ # : (String? value) -> untyped
41
+ def deserialize: (String? value) -> untyped
42
+
43
+ # Encodes a message for storage.
44
+ #
45
+ # Defaults are emitted so the stored shape does not depend on which fields happened to be
46
+ # set, which is what makes {#changed_in_place?} meaningful and spares anything reading the
47
+ # column in SQL from telling absent from default.
48
+ #
49
+ # @param value [Object, nil] the message to store
50
+ # @return [String, nil] the document to write, or nil
51
+ # : (untyped value) -> String?
52
+ def serialize: (untyped value) -> String?
53
+
54
+ # Coerces an assigned value into a message, accepting what a form, a fixture or a seed
55
+ # would supply as well as a message itself.
56
+ #
57
+ # @param value [Object, String, Hash, nil]
58
+ # @return [Object, nil] the message, or the value unchanged when it is none of those
59
+ # : (untyped value) -> untyped
60
+ def cast: (untyped value) -> untyped
61
+
62
+ # Compares decoded messages rather than encoded text: a jsonb column hands back a
63
+ # canonicalized document whose key order and spacing need not match what `encode_json`
64
+ # produced, which would otherwise read as a change on every load.
65
+ #
66
+ # @param raw_old_value [String, nil] the document as it was loaded
67
+ # @param new_value [Object, nil] the message currently held
68
+ # @return [Boolean]
69
+ # : (String? raw_old_value, untyped new_value) -> bool
70
+ def changed_in_place?: (String? raw_old_value, untyped new_value) -> bool
71
+ end
72
+ end
@@ -0,0 +1,58 @@
1
+ # Generated from lib/protobufable/problem/information.rb with RBS::Inline
2
+
3
+ module Protobufable
4
+ module Problem
5
+ # Publishes machine-readable data about an occurrence as protobuf messages, in an
6
+ # `information` extension member.
7
+ #
8
+ # RFC 9457 reserves `detail` for prose, so anything a client is meant to act on rather than
9
+ # read goes somewhere else. This packs each message as a `google.protobuf.Any`, which is what
10
+ # lets one member carry `google.rpc.BadRequest`, `ErrorInfo` and whatever else an occurrence
11
+ # has to say without the document declaring a slot for each.
12
+ #
13
+ # @example
14
+ # class InvalidWidget < Errors::ApiError
15
+ # include Protobufable::Problem::Information
16
+ #
17
+ # def problem_information
18
+ # super + [Google::Rpc::BadRequest.new(field_violations: violations)]
19
+ # end
20
+ # end
21
+ #
22
+ # {"type": "...", "status": 400, "information": [
23
+ # {"@type": "type.googleapis.com/google.rpc.BadRequest", "field_violations": [...]}
24
+ # ]}
25
+ #
26
+ # Every hook calls `super`, so this composes with ::Problem::RetryAfter and with another mixin
27
+ # of your own rather than replacing what they contribute.
28
+ #
29
+ # @rbs module-self ::Problem::Detailable
30
+ module Information : ::Problem::Detailable
31
+ # The extension member the packed messages are published in.
32
+ MEMBER: Symbol
33
+
34
+ # Protobuf messages this occurrence publishes. Override and call `super`.
35
+ #
36
+ # @return [Array<Object>]
37
+ # : () -> Array[untyped]
38
+ def problem_information: () -> Array[untyped]
39
+
40
+ # Adds the packed messages to the document, and nothing at all when there are none: an
41
+ # empty member would be noise on every error that carries no detail of this kind.
42
+ #
43
+ # @return [Hash{Symbol => Object}]
44
+ # : () -> Hash[Symbol, untyped]
45
+ def problem_extensions: () -> Hash[Symbol, untyped]
46
+
47
+ # Packs one message as a `google.protobuf.Any` and renders it as ProtoJSON.
48
+ #
49
+ # Field names are preserved rather than lowerCamelCased, matching what the proto_json
50
+ # renderer sends, so a client reads one spelling across the whole API.
51
+ #
52
+ # @param message [Object] a protobuf message
53
+ # @return [Hash] the Any as ProtoJSON, carrying an `@type` alongside the message's fields
54
+ # : (untyped message) -> untyped
55
+ def self.pack: (untyped message) -> untyped
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,44 @@
1
+ # Generated from lib/protobufable/problem/retry_info.rb with RBS::Inline
2
+
3
+ module Protobufable
4
+ module Problem
5
+ # Publishes a retry interval as a `google.rpc.RetryInfo` as well as a Retry-After header.
6
+ #
7
+ # ::Problem::RetryAfter already sends the header and a `retry_after` member, which a generic
8
+ # HTTP client obeys. This adds the form a client generated from the same protos already knows
9
+ # how to read, so the two kinds of caller get the same answer.
10
+ #
11
+ # @example
12
+ # class TooManyRequests < Errors::ApiError
13
+ # include Protobufable::Problem::RetryInfo
14
+ #
15
+ # problem_type :PROBLEM_TYPE_TOO_MANY_REQUESTS
16
+ # end
17
+ #
18
+ # raise TooManyRequests.new(retry_after: 30)
19
+ #
20
+ # `google.rpc` descriptors are not shipped with this gem: they belong to the app that
21
+ # generates googleapis alongside its own protos. The message class is looked up when it is
22
+ # first needed, and the error names what to generate if it is missing.
23
+ #
24
+ # @rbs module-self ::Problem::Detailable
25
+ module RetryInfo : ::Problem::Detailable
26
+ include ::Problem::RetryAfter
27
+
28
+ include Information
29
+
30
+ # The message this publishes.
31
+ MESSAGE: String
32
+
33
+ # @return [Array<Object>] the occurrence's messages, with the retry interval appended
34
+ # : () -> Array[untyped]
35
+ def problem_information: () -> Array[untyped]
36
+
37
+ # @param seconds [Integer] the wait
38
+ # @return [Object] a `google.rpc.RetryInfo` carrying it
39
+ # @raise [Protobufable::Error] when googleapis has not been generated into the pool
40
+ # : (Integer seconds) -> untyped
41
+ def self.message_for: (Integer seconds) -> untyped
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,166 @@
1
+ # Generated from lib/protobufable/problem/typeable.rb with RBS::Inline
2
+
3
+ module Protobufable
4
+ module Problem
5
+ # Derives an error's RFC 9457 type and status from a protobuf enum, whose values carry them
6
+ # as a custom option.
7
+ #
8
+ # This is the catalogue layer ::Problem::Detailable documents: it prepends a module to the
9
+ # error class's singleton that overrides `type` and `status`, and calls `super` for a class
10
+ # the catalogue does not cover, so a literal declaration still works.
11
+ #
12
+ # Applications whose enum is annotated with one option per property override
13
+ # {ClassMethods#problem_uri_annotation} and {ClassMethods#problem_status_annotation} instead.
14
+ #
15
+ # @example
16
+ # module Errors
17
+ # class ApiError < StandardError
18
+ # include Protobufable::Problem::Typeable
19
+ #
20
+ # def self.problem_type_enum = "myapp.api.ProblemType"
21
+ # def self.problem_type_annotation = "myapp.api.problem_type"
22
+ # end
23
+ #
24
+ # class WidgetNotReady < ApiError
25
+ # problem_type :PROBLEM_TYPE_WIDGET_NOT_READY
26
+ # end
27
+ # end
28
+ #
29
+ # The status is an annotation on the value rather than a property of the code path that
30
+ # raised it: two errors needing different statuses are different types. Several values may
31
+ # publish one identifier on purpose, where telling them apart would leak something; because
32
+ # ::Problem::I18nable keys titles by the published identifier, they share a title too, which is
33
+ # the invariant that keeps the distinction hidden.
34
+ #
35
+ # @rbs module-self ::Exception
36
+ # @rbs module-self ::Problem::_DetailableSelf
37
+ module Typeable : ::Exception, ::Problem::_DetailableSelf
38
+ extend ActiveSupport::Concern
39
+
40
+ include ::Problem::I18nable
41
+
42
+ # What one enum value publishes.
43
+ class Entry < Data
44
+ attr_reader uri(): untyped
45
+
46
+ attr_reader http_status(): untyped
47
+
48
+ def self.new: (untyped uri, untyped http_status) -> instance
49
+ | (uri: untyped, http_status: untyped) -> instance
50
+
51
+ def self.members: () -> [ :uri, :http_status ]
52
+
53
+ def members: () -> [ :uri, :http_status ]
54
+ end
55
+
56
+ # Overrides ::Problem::Detailable's literal DSL for a class that declared a value, and falls
57
+ # through to it for one that did not.
58
+ #
59
+ # Prepended to the singleton class rather than defined in ClassMethods so it wins whichever
60
+ # order the concerns were mixed in. A class that spells out a literal keeps it, the rule
61
+ # ::Problem::I18nable already follows, so a codebase can move onto a catalogue gradually.
62
+ #
63
+ # @rbs module-self _TypeableClass
64
+ module Derivation : _TypeableClass
65
+ # The identifier the declared value publishes, which is its annotation rather than its
66
+ # name: several values deliberately publish the same one.
67
+ #
68
+ # @param value [String, nil] a literal declaration, which takes precedence
69
+ # @return [String, nil]
70
+ # : (?String?) -> String?
71
+ def type: (?String?) -> String?
72
+
73
+ # The status the declared value is returned with.
74
+ #
75
+ # @param value [Integer, nil] a literal declaration, which takes precedence
76
+ # @return [Integer, nil]
77
+ # : (?Integer?) -> Integer?
78
+ def status: (?Integer?) -> Integer?
79
+ end
80
+
81
+ # @rbs module-self _TypeableClass
82
+ module ClassMethods : _TypeableClass
83
+ # The full name of the enum this class's catalogue lives in.
84
+ #
85
+ # No default: an app's catalogue is the one thing a gem cannot guess. Override it on the
86
+ # base class the rest of the errors inherit from.
87
+ #
88
+ # @return [String]
89
+ # @raise [Protobufable::Error] when it has not been overridden
90
+ # : () -> String
91
+ def problem_type_enum: () -> String
92
+
93
+ # The full name of the extension carrying a value's uri and http_status as one message.
94
+ #
95
+ # No default either, because nothing generated from the gem's own .proto is shipped while
96
+ # its option number is provisional. See DESIGN.md.
97
+ #
98
+ # @return [String]
99
+ # @raise [Protobufable::Error] when neither this nor the split pair has been overridden
100
+ # : () -> String
101
+ def problem_type_annotation: () -> String
102
+
103
+ # The extension carrying a value's identifier, for a catalogue that spends one option
104
+ # number per property rather than carrying both in one message.
105
+ #
106
+ # Override this and {problem_status_annotation} together, instead of
107
+ # {problem_type_annotation}, when the enum is already annotated that way. An established
108
+ # schema does not have to be rewritten to be read from here.
109
+ #
110
+ # @return [String, nil]
111
+ # : () -> String?
112
+ def problem_uri_annotation: () -> String?
113
+
114
+ # The extension carrying a value's status, alongside {problem_uri_annotation}.
115
+ #
116
+ # @return [String, nil]
117
+ # : () -> String?
118
+ def problem_status_annotation: () -> String?
119
+
120
+ # Declares which enum value this class renders as, or reads back what was declared.
121
+ #
122
+ # An unknown or unannotated value raises here, at class definition time, so a typo fails
123
+ # on boot rather than on the first request that reaches the error.
124
+ #
125
+ # @param value [Symbol, nil] the enum value name
126
+ # @return [Symbol, nil]
127
+ # @raise [Protobufable::Error] when the catalogue does not cover the value
128
+ # : (?Symbol?) -> Symbol?
129
+ def problem_type: (?Symbol?) -> Symbol?
130
+
131
+ # The catalogue this class reads, built from the enum on first use.
132
+ #
133
+ # Memoized on the class that asked rather than in a table keyed by enum name: a reloaded
134
+ # or anonymous class then cannot read a catalogue built for a previous one.
135
+ #
136
+ # @return [Hash{Symbol => Entry}]
137
+ # : () -> Hash[Symbol, Entry]
138
+ def problem_type_catalogue: () -> Hash[Symbol, Entry]
139
+
140
+ # @return [Entry] what the declared value publishes
141
+ # : () -> Entry
142
+ def problem_type_entry: () -> Entry
143
+ end
144
+
145
+ # Reads every annotated value of an enum.
146
+ #
147
+ # @param enum [String] full name of the enum
148
+ # @param annotation [String, nil] full name of the single sub-message extension
149
+ # @param uri_annotation [String, nil] full name of the identifier extension
150
+ # @param status_annotation [String, nil] full name of the status extension
151
+ # @return [Hash{Symbol => Entry}] one entry per value carrying both properties
152
+ # @raise [Protobufable::Error] when a name does not resolve, or the split pair is half given
153
+ # : (enum: String, annotation: String?, uri_annotation: String?, status_annotation: String?) -> Hash[Symbol, Entry]
154
+ def self.build_catalogue: (enum: String, annotation: String?, uri_annotation: String?, status_annotation: String?) -> Hash[Symbol, Entry]
155
+
156
+ # @return [Proc] reads one value's options into an Entry, or nil when it carries none
157
+ # : (untyped pool, String? annotation, String? uri_annotation, String? status_annotation) -> ^(untyped) -> Entry?
158
+ def self.entry_readers: (untyped pool, String? annotation, String? uri_annotation, String? status_annotation) -> ^(untyped) -> Entry?
159
+
160
+ # @return [Google::Protobuf::FieldDescriptor]
161
+ # @raise [Protobufable::Error] when the extension is not in the pool
162
+ # : (untyped pool, String name) -> untyped
163
+ def self.lookup_extension!: (untyped pool, String name) -> untyped
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,8 @@
1
+ # Generated from lib/protobufable/problem.rb with RBS::Inline
2
+
3
+ module Protobufable
4
+ # The problem gem integration: a catalogue backed by a protobuf enum, and protobuf messages in
5
+ # a problem document. Code in here names the problem gem's own constants as `::Problem`.
6
+ module Problem
7
+ end
8
+ end
@@ -0,0 +1,2 @@
1
+ # Generated from lib/protobufable/protovalidate.rb with RBS::Inline
2
+
@@ -0,0 +1,8 @@
1
+ # Generated from lib/protobufable/railtie.rb with RBS::Inline
2
+
3
+ module Protobufable
4
+ # Installs the renderers and the request body parser on boot, and classifies a broken
5
+ # `buf.validate` rule as a 400.
6
+ class Railtie < ::Rails::Railtie
7
+ end
8
+ end
@@ -0,0 +1,22 @@
1
+ # Generated from lib/protobufable/renderers.rb with RBS::Inline
2
+
3
+ module Protobufable
4
+ # The media types and renderers for answering with a protobuf message.
5
+ #
6
+ # @example Binary, for a client that asked for it
7
+ # render protobuf: Api::GetWidgetResponse.new(widget:)
8
+ #
9
+ # @example ProtoJSON, which is ordinary JSON a client needs to know nothing about
10
+ # render proto_json: Api::GetWidgetResponse.new(widget:)
11
+ module Renderers
12
+ # The media type the binary renderer answers with.
13
+ CONTENT_TYPE: String
14
+
15
+ # Registers the media type and both renderers. Idempotent, so a second host installing on
16
+ # the same process is harmless.
17
+ #
18
+ # @return [void]
19
+ # : () -> void
20
+ def self.install!: () -> void
21
+ end
22
+ end