twirp-on-rails 1.1.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f09ec24234c61ab85dc6ab6e03768c16e4fa7110fe5e5771240ca94633996eb
4
- data.tar.gz: 569db17177ad607e4de0a9f94cf380857c48e408f5e7937ed951bd549d5a0431
3
+ metadata.gz: c7200f05274651a90db8bd46ff410667dce35b41bb49da9a084f7a5c35fd6e1b
4
+ data.tar.gz: d7a993fea396f5449315431cb545f49f0b417ee31aa82b7626d451a65a224985
5
5
  SHA512:
6
- metadata.gz: 542712540c037f4be6ac5f322f45f72a44f55705eb813f941769317afcb17ce1853089524cdf725c61bad411fc73a8ae8be7428a8b3846df6ae9604581d5f90e
7
- data.tar.gz: be401e0761378d95a08bc4d493582450cf5e979e288f578d63c25dd00a69bcc7033b16f77fb31bf0100c2dc1c5a7007fe1c69a8140410b0b4ee5f2238ecfd499
6
+ metadata.gz: 4cb878f46022e40efe060ee312954d2bc6432ecd7fffbac2949173e2f13531f4992665fd2e218d7d6b5308740df5e6acd1ed46b7310ed03a1429f435e94c27a0
7
+ data.tar.gz: 6a803bf6af2df2a8594cfcbc0b8844fc02651d1fc78d6241a25bd80ed9885b8cb3c0f63fba11bb1351000caf62f53400fc46f4478cb7713af6963f6c2e3912e0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.3.0] - 2025-01-20
4
+
5
+ - Add some logging.
6
+
7
+ ## [1.2.0] - 2024-10-04
8
+
9
+ - Support rescue_from in handlers.
10
+
3
11
  ## [1.1.0] - 2024-09-12
4
12
 
5
13
  - Respect package namespace when looking for handlers.
data/Gemfile CHANGED
@@ -9,7 +9,11 @@ gem "rake"
9
9
 
10
10
  gem "debug"
11
11
  gem "rspec-rails"
12
- gem "sqlite3", "~> 1.4"
13
12
  gem "standard", ">= 1.35.1"
14
13
  gem "standard-performance"
15
14
  gem "standard-rails"
15
+
16
+ # These standard library gems need to be here for with certain Rails 7/Ruby 3.4 combinations. Delete eventually.
17
+ gem "mutex_m"
18
+ gem "bigdecimal"
19
+ gem "drb"
data/README.md CHANGED
@@ -111,6 +111,22 @@ TODO: Give more examples of handlers
111
111
 
112
112
  Use `before_action`, `around_action`, and other callbacks you're used to, as we build on [AbstractController::Callbacks](https://api.rubyonrails.org/classes/AbstractController/Callbacks.html).
113
113
 
114
+ ### rescue_from
115
+
116
+ Use `rescue_from` just like you would in a controller:
117
+
118
+ ```ruby
119
+ class HaberdasherServiceHandler < Twirp::Rails::Handler
120
+ rescue_from "ArgumentError" do |error|
121
+ Twirp::Error.invalid_argument(error.message)
122
+ end
123
+
124
+ rescue_from "Pundit::NotAuthorizedError", :not_authorized
125
+
126
+ ...
127
+ end
128
+ ```
129
+
114
130
  ### DRY Service Hooks
115
131
 
116
132
  Apply [Service Hooks](https://github.com/twitchtv/twirp-ruby/wiki/Service-Hooks) one time across multiple services.
@@ -135,6 +151,22 @@ As an Engine, we avoid all the standard Rails middleware. That's nice for simpli
135
151
  Rails.application.config.twirp.middleware = [Rack::Deflater]
136
152
  ```
137
153
 
154
+ ### Logging
155
+
156
+ Our built-in logging outputs the result of each request.
157
+
158
+ You could replace our logger if you want different output:
159
+
160
+ ```ruby
161
+ Rails.application.config.twirp.logger = Rack::CommonLogger
162
+ ```
163
+
164
+ Additionally, you can log the full Twirp response object to help with debugging:
165
+
166
+ ```ruby
167
+ Rails.application.config.twirp.verbose_logging = true
168
+ ```
169
+
138
170
  ## Bonus Features
139
171
 
140
172
  Outside the [Twirp spec](https://twitchtv.github.io/twirp/docs/spec_v7.html), we have some (optional) extra magic. They might be useful to you, but you can easily ignore them too.
@@ -154,6 +186,8 @@ Rails.application.config.twirp.middleware = [
154
186
  ]
155
187
  ```
156
188
 
189
+ Note: The Handler will still be run, but you won't need to send back the response. Make sure your RPC is idempotent! Future versions hope to make it easier to short-circuit expensive parts of the handler.
190
+
157
191
  ## TODO
158
192
 
159
193
  * More docs!
@@ -9,6 +9,15 @@ module Twirp
9
9
  # Where to mount twirp routes. Defaults to /twirp
10
10
  attr_accessor :endpoint
11
11
 
12
+ # Logger to use for Twirp requests. Defaults to Rails.logger
13
+ attr_accessor :logger
14
+
15
+ # Whether to log full Twirp responses. Can be useful for debugging, but can expose sensitive data.
16
+ # Defauts to false
17
+ # Example:
18
+ # Twirp Response: <Twirp::Example::Haberdasher::Hat: inches: 24, color: "Tan", name: "Pork Pie">
19
+ attr_accessor :verbose_logging
20
+
12
21
  # An array of directories to search for *_twirp.rb files
13
22
  # Defaults to ["lib"]
14
23
  attr_accessor :load_paths
@@ -25,6 +34,8 @@ module Twirp
25
34
  @auto_mount = false
26
35
  @endpoint = "/twirp"
27
36
  @load_paths = ["lib"]
37
+ @logger = Logger
38
+ @verbose_logging = false
28
39
  @middleware = []
29
40
  @service_hooks = {}
30
41
  end
@@ -26,26 +26,45 @@ module Twirp
26
26
  app.config.twirp.send(key)
27
27
  end
28
28
 
29
+ # Set up logging
30
+ app.config.middleware.use app.config.twirp.logger, ::Rails.logger
29
31
  app.config.twirp.middleware.each do |middleware|
30
32
  app.config.middleware.use middleware
31
33
  end
34
+
35
+ # Load all Twirp files
36
+ app.config.twirp.load_paths.each do |directory|
37
+ ::Rails.root.glob("#{directory}/**/*_twirp.rb").sort.each { |file| require file }
38
+ end
32
39
  end
33
40
  end
34
41
 
35
42
  class << self
36
43
  def services
37
44
  if @services.nil?
38
- ::Rails.application.config.twirp.load_paths.each do |directory|
39
- ::Rails.root.glob("#{directory}/**/*_twirp.rb").sort.each { |file| require file }
40
- end
41
-
42
45
  @services = Twirp::Service.subclasses.map(&:new)
43
46
 
44
47
  # Install hooks that may be defined in the config
45
48
  @services.each do |service|
49
+ # Add user-defined hooks
46
50
  ::Rails.application.config.twirp.service_hooks.each do |hook_name, hook|
47
51
  service.send(hook_name, &hook)
48
52
  end
53
+
54
+ # Add our own logging hooks
55
+ service.on_success do |env|
56
+ if ::Rails.application.config.twirp.verbose_logging
57
+ ::Rails.logger.debug { "Twirp Response: #{env[:output].inspect}" }
58
+ end
59
+ end
60
+
61
+ service.on_error do |error, _env|
62
+ ::Rails.logger.debug { "Twirp Response: #{error.inspect}" }
63
+ end
64
+
65
+ service.exception_raised do |exception, _env|
66
+ ::Rails.logger.error { "Twirp Exception (#{exception.class}: #{exception.message})\n#{exception.backtrace.join("\n")}" }
67
+ end
49
68
  end
50
69
  end
51
70
 
@@ -54,11 +73,3 @@ module Twirp
54
73
  end
55
74
  end
56
75
  end
57
-
58
- class Twirp::Service
59
- # Override inspect to show all available RPCs
60
- # This is used when displaying routes.
61
- def inspect
62
- self.class.rpcs.map { |rpc| "#{self.class.name.demodulize.underscore}_handler##{rpc[1][:ruby_method]}" }.join("\n")
63
- end
64
- end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twirp
4
+ module Rails
5
+ class Error < StandardError
6
+ end
7
+ end
8
+ end
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "rescuable" # Ruby 2.7 fails without this. Remove eventually.
4
+
3
5
  module Twirp
4
6
  module Rails
5
7
  class Handler
6
8
  include Twirp::Rails::Callbacks
9
+ include ActiveSupport::Rescuable
10
+ using Twirp::Rails::Rescuable
7
11
 
8
12
  attr_reader :request, :env
9
13
  attr_reader :action_name
@@ -34,7 +38,11 @@ module Twirp
34
38
  ActiveSupport::Notifications.instrument("handler_run_callbacks.twirp_rails", handler: self.class.name, action: action_name, env: @env, request: @request) do
35
39
  run_callbacks(:process_action) do
36
40
  ActiveSupport::Notifications.instrument("handler_run.twirp_rails", handler: self.class.name, action: action_name, env: @env, request: @request) do |payload|
37
- payload[:response] = send_action(name)
41
+ payload[:response] = begin
42
+ send_action(name)
43
+ rescue => exception
44
+ rescue_with_handler_and_return(exception) || raise
45
+ end
38
46
  end
39
47
  end
40
48
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Rack::CommonLogger is nice but we can do better.
4
+ # Rails doesn't use it, but we need to log Twirp requests.
5
+ # Here's an example from Rack::CommonLogger:
6
+ # 127.0.0.1 - - [12/Jan/2025:17:09:49 -0500] "POST /twirp/twirp.example.haberdasher.Haberdasher/MakeHat HTTP/1.0" 200 - 439.0060
7
+ #
8
+ # Rails gives us this:
9
+ # Started POST "/twirp/twirp.example.haberdasher.Haberdasher/MakeHat" for 127.0.0.1 at 2025-01-12 22:48:00 -0500
10
+ # but we also want to know the result of the Twirp call.
11
+ # Here's what this Logger adds:
12
+ # Twirp 200 in 2ms as application/protobuf
13
+ #
14
+
15
+ module Twirp
16
+ module Rails
17
+ class Logger < ::Rack::CommonLogger
18
+ private
19
+
20
+ def log(env, status, response_headers, began_at)
21
+ content_type = response_headers["content-type"].presence
22
+ content_encoding = response_headers["content-encoding"].presence
23
+ @logger.info { "Twirp #{status} in #{duration_in_ms(began_at)}ms#{" as #{content_type}" if content_type}#{" with content-encoding: #{content_encoding}" if content_encoding}" }
24
+ end
25
+
26
+ def duration_in_ms(time)
27
+ ((::Rack::Utils.clock_time - time) * 1000).to_i
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twirp
4
+ module Rails
5
+ module Rescuable
6
+ refine ::ActiveSupport::Rescuable::ClassMethods do
7
+ # A slightly altered version of ActiveSupport::Rescuable#rescue_with_handler
8
+ # that returns the result rather than the handled exception
9
+ def rescue_with_handler_and_return(exception, object: self, visited_exceptions: [])
10
+ visited_exceptions << exception
11
+
12
+ if (handler = handler_for_rescue(exception, object: object))
13
+ handler.call exception
14
+ elsif exception
15
+ if visited_exceptions.include?(exception.cause)
16
+ nil
17
+ else
18
+ rescue_with_handler(exception.cause, object: object, visited_exceptions: visited_exceptions)
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ refine ::ActiveSupport::Rescuable do
25
+ def rescue_with_handler_and_return(exception)
26
+ self.class.rescue_with_handler_and_return exception, object: self
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Twirp
4
4
  module Rails
5
- VERSION = "1.1.0"
5
+ VERSION = "1.3.0"
6
6
  end
7
7
  end
data/lib/twirp/rails.rb CHANGED
@@ -1,30 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "rails/version"
3
+ require "twirp"
4
+ require "active_support/notifications"
4
5
 
5
- module Twirp
6
- module Rails
7
- class Error < StandardError; end
8
- # Your code goes here...
9
- end
6
+ require "zeitwerk"
7
+ loader = Zeitwerk::Loader.for_gem_extension(Twirp)
8
+ loader.ignore("#{__dir__}/on/rails.rb")
9
+ loader.setup
10
+
11
+ module Twirp::Rails
10
12
  end
11
13
 
12
- require "twirp"
13
- require "active_support/notifications"
14
- require_relative "rails/callbacks"
15
- require_relative "rails/configuration"
16
- require_relative "rails/dispatcher"
17
- require_relative "rails/engine"
18
- require_relative "rails/handler"
14
+ loader.eager_load
15
+
16
+ Twirp::Service.class_eval do
17
+ # Override initialize to make handler argument optional.
18
+ # When left nil, we will use our dispatcher.
19
+ alias_method :original_initialize, :initialize
20
+ def initialize(handler = nil)
21
+ handler ||= Twirp::Rails::Dispatcher.new(self.class)
22
+ original_initialize(handler)
23
+ end
19
24
 
20
- module Twirp
21
- class Service
22
- # Override initialize to make handler argument optional.
23
- # When left nil, we will use our dispatcher.
24
- alias_method :original_initialize, :initialize
25
- def initialize(handler = nil)
26
- handler ||= Twirp::Rails::Dispatcher.new(self.class)
27
- original_initialize(handler)
28
- end
25
+ # Override inspect to show all available RPCs
26
+ # This is used when displaying routes.
27
+ def inspect
28
+ self.class.rpcs.map { |rpc| "#{self.class.name.demodulize.underscore}_handler##{rpc[1][:ruby_method]}" }.join("\n")
29
29
  end
30
30
  end
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: twirp-on-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Morrison
8
8
  - Darron Schall
9
- autorequire:
10
9
  bindir: exe
11
10
  cert_chain: []
12
- date: 2024-09-12 00:00:00.000000000 Z
11
+ date: 2025-01-20 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: rails
@@ -61,8 +60,11 @@ files:
61
60
  - lib/twirp/rails/configuration.rb
62
61
  - lib/twirp/rails/dispatcher.rb
63
62
  - lib/twirp/rails/engine.rb
63
+ - lib/twirp/rails/error.rb
64
64
  - lib/twirp/rails/handler.rb
65
+ - lib/twirp/rails/logger.rb
65
66
  - lib/twirp/rails/rack/conditional_post.rb
67
+ - lib/twirp/rails/rescuable.rb
66
68
  - lib/twirp/rails/version.rb
67
69
  - sig/twirp/rails.rbs
68
70
  - twirp-rails.gemspec
@@ -73,7 +75,6 @@ metadata:
73
75
  homepage_uri: https://github.com/collectiveidea/twirp-rails
74
76
  source_code_uri: https://github.com/collectiveidea/twirp-rails
75
77
  changelog_uri: https://github.com/collectiveidea/twirp-rails/blob/main/CHANGELOG.md
76
- post_install_message:
77
78
  rdoc_options: []
78
79
  require_paths:
79
80
  - lib
@@ -88,8 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
88
89
  - !ruby/object:Gem::Version
89
90
  version: '0'
90
91
  requirements: []
91
- rubygems_version: 3.5.16
92
- signing_key:
92
+ rubygems_version: 3.6.2
93
93
  specification_version: 4
94
94
  summary: Use Twirp RPC with Rails
95
95
  test_files: []