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,131 @@
1
+ # Generated from lib/protobufable/request_parseable.rb with RBS::Inline
2
+
3
+ module Protobufable
4
+ # Parses a JSON request body straight into a generated protobuf message, so a wrong type or
5
+ # an unknown field is rejected before the action runs and the action reads a typed object
6
+ # instead of `params`.
7
+ #
8
+ # The declaration sits above the action it describes, like a signature:
9
+ #
10
+ # @example
11
+ # class WidgetsController < ApplicationController
12
+ # include Protobufable::RequestParseable
13
+ #
14
+ # protobuf_body Api::CreateWidgetRequest
15
+ # def create
16
+ # Widget.create!(name: message.name)
17
+ # end
18
+ # end
19
+ #
20
+ # {RequestPatch} has to be prepended to `ActionDispatch::Request` for any of this to happen;
21
+ # {Protobufable.install!} does that, and the railtie calls it.
22
+ #
23
+ # @rbs module-self _RequestParseableSelf
24
+ module RequestParseable : _RequestParseableSelf
25
+ extend ActiveSupport::Concern
26
+
27
+ # What `protobuf_body` records about one action.
28
+ class Declaration < Data
29
+ attr_reader message_class(): untyped
30
+
31
+ attr_reader ignore_unknown_fields(): untyped
32
+
33
+ def self.new: (untyped message_class, untyped ignore_unknown_fields) -> instance
34
+ | (message_class: untyped, ignore_unknown_fields: untyped) -> instance
35
+
36
+ def self.members: () -> [ :message_class, :ignore_unknown_fields ]
37
+
38
+ def members: () -> [ :message_class, :ignore_unknown_fields ]
39
+ end
40
+
41
+ # Holds the parsed message in `params` without letting it be logged.
42
+ #
43
+ # Rails' parameter filtering cannot see inside a protobuf message, so a message reaching
44
+ # the log formatter would print every field it holds, secrets included.
45
+ class MessageWrapper
46
+ # Stands in for the message wherever `params` is rendered.
47
+ FILTERED: String
48
+
49
+ # @param message [Object] the parsed protobuf message
50
+ # @return [void]
51
+ # : (untyped message) -> void
52
+ def initialize: (untyped message) -> void
53
+
54
+ # @return [Array] always empty, so `inspect` does not reach the message
55
+ # : () -> Array[Symbol]
56
+ def instance_variables_to_inspect: () -> Array[Symbol]
57
+
58
+ # The JSON log formatter renders payloads with ActiveSupport's `#to_json`, and
59
+ # `Object#as_json` falls back to dumping instance variables.
60
+ #
61
+ # @return [String] {FILTERED}
62
+ # : (*untyped) -> String
63
+ def as_json: (*untyped) -> String
64
+
65
+ # @return [Object] the wrapped message
66
+ # : () -> untyped
67
+ def to_protobuf: () -> untyped
68
+
69
+ # @return [Object] the wrapped message
70
+ # : () -> untyped
71
+ def to_original_message: () -> untyped
72
+ end
73
+
74
+ # Replaces the `:json` body parser so the body is decoded once, into the declared class,
75
+ # rather than parsed as JSON and then converted.
76
+ module RequestPatch
77
+ # : () -> Hash[Symbol, untyped]
78
+ private def params_parsers: () -> Hash[Symbol, untyped]
79
+ end
80
+
81
+ # Decodes the body for a request whose action declared a message class, and falls through
82
+ # to the original parser for every other request.
83
+ #
84
+ # The declaration is read off the controller class the request already resolves, rather
85
+ # than a process-wide table keyed by controller path: a table outlives reloads and renames,
86
+ # and cannot describe an anonymous controller at all.
87
+ #
88
+ # @param request [ActionDispatch::Request]
89
+ # @param raw_post [String] the request body
90
+ # @param original_parser [Proc] the parser this one stands in front of
91
+ # @return [Hash] the request parameters, carrying the message under `:_protobuf`
92
+ # : (untyped request, String raw_post, ^(String) -> untyped original_parser) -> untyped
93
+ def self.param_parser: (untyped request, String raw_post, ^(String) -> untyped original_parser) -> untyped
94
+
95
+ # @param request [ActionDispatch::Request]
96
+ # @return [Declaration, nil] what the routed action declared, if anything
97
+ # : (untyped request) -> Declaration?
98
+ def self.declaration_for: (untyped request) -> Declaration?
99
+
100
+ # Class-level DSL installed by including the concern.
101
+ #
102
+ # @rbs module-self _RequestParseableClass
103
+ module ClassMethods : _RequestParseableClass
104
+ # Declares the message class the next action accepts.
105
+ #
106
+ # @param message_class [Class] the generated request message class
107
+ # @param ignore_unknown_fields [Boolean] whether a field the schema does not declare is
108
+ # dropped rather than rejected. False by default, so a client's typo is a 400 rather
109
+ # than a value that silently never arrives.
110
+ # @return [void]
111
+ # : (Google::Protobuf::_MessageClass message_class, ?ignore_unknown_fields: bool) -> void
112
+ def protobuf_body: (Google::Protobuf::_MessageClass message_class, ?ignore_unknown_fields: bool) -> void
113
+
114
+ # Binds a pending `protobuf_body` to the action defined under it.
115
+ #
116
+ # @param method_name [Symbol]
117
+ # @return [void]
118
+ # : (Symbol method_name) -> void
119
+ def method_added: (Symbol method_name) -> void
120
+ end
121
+
122
+ # Reads the message from the parsed body only: `params` also holds the query string, where a
123
+ # `_protobuf` parameter would otherwise stand in for the message.
124
+ #
125
+ # @return [Object] the parsed message for this action
126
+ # @raise [KeyError] when the action declared no message class
127
+ # @raise [ActionDispatch::Http::Parameters::ParseError] when the request carried no JSON body
128
+ # : () -> untyped
129
+ def message: () -> untyped
130
+ end
131
+ end
@@ -0,0 +1,71 @@
1
+ # Generated from lib/protobufable/request_validatable.rb with RBS::Inline
2
+
3
+ module Protobufable
4
+ # Runs the `buf.validate` rules of a `protobuf_body` message before the action does.
5
+ #
6
+ # Decoding already rejects a wrong type; this adds what the rules declare — lengths, formats,
7
+ # ranges, cross-field conditions — so value validation lives in the schema next to the type
8
+ # rather than in the controller.
9
+ #
10
+ # @example
11
+ # class WidgetsController < ApplicationController
12
+ # include Protobufable::RequestValidatable
13
+ #
14
+ # protobuf_body Api::CreateWidgetRequest
15
+ # def create = ...
16
+ # end
17
+ #
18
+ # Raises {InvalidMessage}, which carries the violations. Rescue it wherever the application
19
+ # turns errors into responses.
20
+ #
21
+ # @rbs module-self _RequestValidatableSelf
22
+ module RequestValidatable : _RequestValidatableSelf
23
+ extend ActiveSupport::Concern
24
+
25
+ include RequestParseable
26
+
27
+ # Raised when the request message breaks a rule its schema declares.
28
+ class InvalidMessage < Protobufable::Error
29
+ # @return [Array<Protovalidate::Violation>]
30
+ attr_reader violations: Array[untyped]
31
+
32
+ # @return [Class] the message the violations were produced from
33
+ attr_reader message_class: Google::Protobuf::_MessageClass
34
+
35
+ # @param violations [Array<Protovalidate::Violation>]
36
+ # @param message_class [Class]
37
+ # @return [void]
38
+ # : (Array[untyped] violations, Google::Protobuf::_MessageClass message_class) -> void
39
+ def initialize: (Array[untyped] violations, Google::Protobuf::_MessageClass message_class) -> void
40
+ end
41
+
42
+ # @rbs module-self _RequestValidatableClass
43
+ module ClassMethods : _RequestValidatableClass
44
+ # Re-registers the validation callback behind the action's own declaration.
45
+ #
46
+ # Rails appends a re-registered callback to the end of the chain. Without this, the
47
+ # callback stays where `included` put it — ahead of the authentication and authorization
48
+ # callbacks a controller declares afterwards — and an invalid body would be answered
49
+ # before an unauthenticated one, turning the endpoint into an oracle for what a valid
50
+ # body looks like.
51
+ #
52
+ # A controller that skipped the callback has no entry to move, and is left alone.
53
+ #
54
+ # @param method_name [Symbol]
55
+ # @return [void]
56
+ # : (Symbol method_name) -> void
57
+ def method_added: (Symbol method_name) -> void
58
+
59
+ # @return [Boolean] whether a `protobuf_body` is waiting to bind to the next action
60
+ # : () -> bool
61
+ def protobuf_body_pending?: () -> bool
62
+ end
63
+
64
+ # Validates the parsed message, if this action declared one.
65
+ #
66
+ # @return [void]
67
+ # @raise [InvalidMessage] when the message breaks a rule
68
+ # : () -> void
69
+ private def validate_protobuf_body!: () -> void
70
+ end
71
+ end
@@ -0,0 +1,6 @@
1
+ # Generated from lib/protobufable/version.rb with RBS::Inline
2
+
3
+ module Protobufable
4
+ # The gem's version.
5
+ VERSION: String
6
+ end
@@ -0,0 +1,24 @@
1
+ # Generated from lib/protobufable.rb with RBS::Inline
2
+
3
+ # Protocol Buffers as the schema of record for a Rails API: request bodies, stored columns,
4
+ # error catalogues and responses all described by generated message classes.
5
+ #
6
+ # Each integration is required on its own, so a host pays only for what it uses:
7
+ #
8
+ # require "protobufable/problem" # needs the problem gem
9
+ # require "protobufable/protovalidate" # needs the protovalidate gem
10
+ # require "protobufable/alba_binding" # needs the alba gem
11
+ module Protobufable
12
+ # Raised when a declaration cannot be honoured, such as an enum value the catalogue does not
13
+ # define or a serializer that has drifted from its message.
14
+ class Error < StandardError
15
+ end
16
+
17
+ # Registers the renderers and installs the request body parser. Called by the railtie on
18
+ # boot; a Rack host, or a spec that never boots Rails, calls it itself. Installing twice is
19
+ # harmless.
20
+ #
21
+ # @return [void]
22
+ # : () -> void
23
+ def self.install!: () -> void
24
+ end
@@ -0,0 +1,29 @@
1
+ # Alba ships no signatures and is not in ruby/gem_rbs_collection. Only what this gem touches
2
+ # is declared, and the resource DSL stays untyped: it is Alba's to describe, not ours.
3
+ module Alba
4
+ def self.register_type: (Symbol name, converter: untyped, ?auto_convert: bool) -> void
5
+
6
+ module Resource
7
+ end
8
+ end
9
+
10
+ module Protobufable
11
+ module AlbaBinding
12
+ # What an Alba resource instance supplies. A module self-type constrains includers, not the
13
+ # module itself, so it is spelled out here.
14
+ interface _AlbaBindingSelf
15
+ def class: () -> _AlbaBindingClass
16
+ def as_json: (*untyped) -> untyped
17
+ end
18
+
19
+ # The class side: the binding this installs, plus the parts of Class the DSL uses.
20
+ interface _AlbaBindingClass
21
+ def message_class: () -> Google::Protobuf::_MessageClass
22
+ def name: () -> String?
23
+ def module_eval: () { () -> void } -> untyped
24
+ def helper: (Module) -> void
25
+ def instance_variable_get: (Symbol) -> untyped
26
+ def class_eval: () { () -> void } -> untyped
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ module Protobufable
2
+ module Connect
3
+ module ProblemRescuable
4
+ # What a controller including the concern supplies: ::Problem::Rescuable's needs, plus the
5
+ # renderer ConnectRpcRails::Controller defines.
6
+ interface _ProblemRescuableSelf
7
+ include ::Problem::_RescuableSelf
8
+
9
+ def render_connect_error: (::ConnectRpcRails::Error) -> untyped
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ # The part of connect_rpc_rails the Connect integration calls. Declared here rather than loaded
2
+ # from the gem: see rbs_collection.yaml.
3
+ module ConnectRpcRails
4
+ class Error < StandardError
5
+ def self.code_for_http_status: (Integer) -> Symbol
6
+
7
+ def initialize: (Symbol code, ?String? message, ?details: Array[untyped]) -> void
8
+ end
9
+ end
@@ -0,0 +1,28 @@
1
+ module Protobufable
2
+ module Problem
3
+ module Typeable
4
+ # The class-level state Typeable installs, plus the parts of ::Problem::Detailable's
5
+ # DSL that Derivation reaches through `super` or reads to decide whether to. Declared by
6
+ # hand because `class_attribute` defines its accessors at runtime, and because a module
7
+ # self-type constrains includers rather than the module itself.
8
+ interface _TypeableClass
9
+ def name: () -> String?
10
+
11
+ def problem_type_value: () -> Symbol?
12
+ def problem_type_value=: (Symbol?) -> void
13
+
14
+ def problem_type: (?Symbol?) -> Symbol?
15
+ def problem_type_enum: () -> String
16
+ def problem_type_annotation: () -> String
17
+ def problem_uri_annotation: () -> String?
18
+ def problem_status_annotation: () -> String?
19
+ def problem_type_catalogue: () -> Hash[Symbol, Entry]
20
+ def problem_type_entry: () -> Entry
21
+
22
+ # Reached by Derivation, which decides between the catalogue and a literal declaration.
23
+ def problem_uri: () -> String?
24
+ def problem_status: () -> Integer?
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,12 @@
1
+ module Protobufable
2
+ # Generated from proto/protobufable/problem_details.proto. Hand-written because the generation
3
+ # pipeline emits Ruby only; problem_details_spec.rb checks the two still agree.
4
+ class ProblemDetails < ::Google::Protobuf::AbstractMessage
5
+ attr_reader type: ::String
6
+ attr_reader title: ::String
7
+ attr_reader status: ::Integer
8
+ attr_reader detail: ::String
9
+ attr_reader instance: ::String
10
+ attr_reader information: ::Google::Protobuf::RepeatedField[::Google::Protobuf::Any]
11
+ end
12
+ end
@@ -0,0 +1,8 @@
1
+ module Protobufable
2
+ # Generated from proto/protobufable/problem.proto. Hand-written because the generation
3
+ # pipeline emits Ruby only; problem_type_options_spec.rb checks the two still agree.
4
+ class ProblemTypeOptions < ::Google::Protobuf::AbstractMessage
5
+ attr_reader uri: ::String
6
+ attr_reader http_status: ::Integer
7
+ end
8
+ end
@@ -0,0 +1,38 @@
1
+ module Protobufable
2
+ module RequestValidatable
3
+ # What a controller including the concern supplies. A module self-type constrains
4
+ # includers, not the module itself, so these are spelled out here.
5
+ interface _RequestValidatableSelf
6
+ include RequestParseable::_RequestParseableSelf
7
+
8
+ def action_name: () -> String
9
+ def message: () -> untyped
10
+ def class: () -> _RequestValidatableClass
11
+ end
12
+
13
+ # The class-level state this reads, both its own and RequestParseable's. Declared by hand
14
+ # because `class_attribute` and the callback DSL define their methods at runtime.
15
+ interface _RequestValidatableClass
16
+ include RequestParseable::_RequestParseableClass
17
+
18
+ def before_action: (*untyped) -> void
19
+ def _process_action_callbacks: () -> untyped
20
+ def instance_variable_get: (Symbol) -> untyped
21
+ def protobuf_body_pending?: () -> bool
22
+ end
23
+ end
24
+
25
+ module ColumnValidatable
26
+ # The attribute table the validation walks, which ActiveRecord defines on the class.
27
+ interface _ColumnValidatableClass
28
+ def attribute_types: () -> Hash[String, untyped]
29
+ end
30
+
31
+ # What an ActiveRecord model supplies to the validation this installs.
32
+ interface _ColumnValidatableSelf
33
+ def class: () -> _ColumnValidatableClass
34
+ def []: (String name) -> untyped
35
+ def errors: () -> untyped
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,17 @@
1
+ module Protobufable
2
+ module RequestParseable
3
+ # What `params` a controller including the concern supplies. A module self-type constrains
4
+ # includers, not the module's own type, so it has to be spelled out here.
5
+ interface _RequestParseableSelf
6
+ def params: () -> untyped
7
+ def request: () -> untyped
8
+ end
9
+
10
+ # The class-level state RequestParseable installs. Declared by hand because
11
+ # `class_attribute` defines its accessors at runtime, which RBS cannot infer.
12
+ interface _RequestParseableClass
13
+ def protobuf_body_declarations: () -> Hash[String, Declaration]
14
+ def protobuf_body_declarations=: (Hash[String, Declaration]) -> void
15
+ end
16
+ end
17
+ end