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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +30 -0
- data/DESIGN.md +298 -0
- data/LICENSE.txt +21 -0
- data/README.md +229 -0
- data/docs/columns.md +34 -0
- data/docs/connect.md +55 -0
- data/docs/errors.md +190 -0
- data/docs/request-bodies.md +42 -0
- data/docs/responses.md +49 -0
- data/docs/validation.md +121 -0
- data/lib/protobufable/alba_binding.rb +149 -0
- data/lib/protobufable/bad_request.rb +60 -0
- data/lib/protobufable/column_validatable.rb +56 -0
- data/lib/protobufable/connect/problem_rescuable.rb +92 -0
- data/lib/protobufable/connect.rb +20 -0
- data/lib/protobufable/field_path.rb +65 -0
- data/lib/protobufable/json_type.rb +114 -0
- data/lib/protobufable/problem/information.rb +75 -0
- data/lib/protobufable/problem/retry_info.rb +62 -0
- data/lib/protobufable/problem/typeable.rb +259 -0
- data/lib/protobufable/problem.rb +27 -0
- data/lib/protobufable/problem_details_pb.rb +17 -0
- data/lib/protobufable/problem_pb.rb +15 -0
- data/lib/protobufable/protovalidate.rb +15 -0
- data/lib/protobufable/railtie.rb +26 -0
- data/lib/protobufable/renderers.rb +63 -0
- data/lib/protobufable/request_parseable.rb +188 -0
- data/lib/protobufable/request_validatable.rb +111 -0
- data/lib/protobufable/version.rb +10 -0
- data/lib/protobufable.rb +40 -0
- data/lib/tapioca/dsl/compilers/protobufable_request_parseable.rb +69 -0
- data/proto/protobufable/problem.proto +36 -0
- data/proto/protobufable/problem_details.proto +31 -0
- data/sig/generated/protobufable/alba_binding.rbs +101 -0
- data/sig/generated/protobufable/bad_request.rbs +37 -0
- data/sig/generated/protobufable/column_validatable.rbs +23 -0
- data/sig/generated/protobufable/connect/problem_rescuable.rbs +49 -0
- data/sig/generated/protobufable/connect.rbs +7 -0
- data/sig/generated/protobufable/field_path.rbs +33 -0
- data/sig/generated/protobufable/json_type.rbs +72 -0
- data/sig/generated/protobufable/problem/information.rbs +58 -0
- data/sig/generated/protobufable/problem/retry_info.rbs +44 -0
- data/sig/generated/protobufable/problem/typeable.rbs +166 -0
- data/sig/generated/protobufable/problem.rbs +8 -0
- data/sig/generated/protobufable/protovalidate.rbs +2 -0
- data/sig/generated/protobufable/railtie.rbs +8 -0
- data/sig/generated/protobufable/renderers.rbs +22 -0
- data/sig/generated/protobufable/request_parseable.rbs +131 -0
- data/sig/generated/protobufable/request_validatable.rbs +71 -0
- data/sig/generated/protobufable/version.rbs +6 -0
- data/sig/generated/protobufable.rbs +24 -0
- data/sig/manual/alba_binding.rbs +29 -0
- data/sig/manual/connect/problem_rescuable.rbs +13 -0
- data/sig/manual/connect_rpc_rails.rbs +9 -0
- data/sig/manual/problem/typeable.rbs +28 -0
- data/sig/manual/problem_details_pb.rbs +12 -0
- data/sig/manual/problem_pb.rbs +8 -0
- data/sig/manual/protovalidate.rbs +38 -0
- data/sig/manual/request_parseable.rbs +17 -0
- 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,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,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
|