Skip to content

Commit

Permalink
Merge pull request #9 from collectiveidea/active-support-notifications
Browse files Browse the repository at this point in the history
Instrument with ActiveSupport::Notifications
  • Loading branch information
danielmorrison authored Dec 6, 2023
2 parents 2b9a947 + e2208da commit 5323f52
Show file tree
Hide file tree
Showing 12 changed files with 149 additions and 11 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ jobs:
strategy:
matrix:
ruby:
- '3.1.3'
- '3.2.0'
- 2.7
- '3.0' # keep as string or it turns into "3" which pulls the newest 3.x, not 3.0.x
- 3.1
- 3.2

env:
RUBYOPT: --enable=frozen-string-literal
Expand Down
5 changes: 4 additions & 1 deletion .standard.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# For available configuration options, see:
# https://github.com/testdouble/standard
ruby_version: 2.6
ruby_version: 2.7
plugins:
- standard-performance
- standard-rails
8 changes: 5 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ source "https://rubygems.org"
# Specify your gem's dependencies in twirp-rails.gemspec
gemspec

gem "rake", "~> 13.0"
gem "rake"

gem "debug"
gem "rspec-rails", "~> 3.0"
gem "rspec-rails"
gem "sqlite3"
gem "standard", "~> 1.3"
gem "standard"
gem "standard-performance"
gem "standard-rails"
1 change: 1 addition & 0 deletions lib/twirp/rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Error < StandardError; end
end

require "twirp"
require "active_support/notifications"
require_relative "rails/callbacks"
require_relative "rails/configuration"
require_relative "rails/dispatcher"
Expand Down
1 change: 1 addition & 0 deletions lib/twirp/rails/callbacks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ module Callbacks
terminator: ->(controller, result_lambda) {
# save off the error and terminate if a callback returns a Twirp::Error
result = result_lambda.call

if result.is_a?(Twirp::Error)
controller.error = result
true
Expand Down
8 changes: 6 additions & 2 deletions lib/twirp/rails/handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ def process(name, request, env)
# Notice that the first argument is the method to be dispatched
# which is *not* necessarily the same as the action name.
def process_action(name)
run_callbacks(:process_action) do
send_action(name)
ActiveSupport::Notifications.instrument("handler_run_callbacks.twirp_rails", handler: self.class.name, action: action_name, env: @env, request: @request) do
run_callbacks(:process_action) do
ActiveSupport::Notifications.instrument("handler_run.twirp_rails", handler: self.class.name, action: action_name, env: @env, request: @request) do |payload|
payload[:response] = send_action(name)
end
end
end
end

Expand Down
19 changes: 19 additions & 0 deletions spec/rails_app/app/handlers/haberdasher_handler.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
require "ip_tracker"
class HaberdasherHandler < Twirp::Rails::Handler
before_action :track_request_ip
before_action :reject_giant_hats

def make_hat
# We can return a Twirp::Error when appropriate
if request.inches < 12
Expand All @@ -12,4 +16,19 @@ def make_hat
color: "Tan"
)
end

private

# Contrived example of using a before_action with data that
# comes from a :before service hook.
def track_request_ip
IPTracker.track(env[:ip])
end

# contrived example of using a before_action
def reject_giant_hats
if request.inches >= 1_000
Twirp::Error.invalid_argument("is too big", argument: "inches")
end
end
end
1 change: 0 additions & 1 deletion spec/rails_app/config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@
module RailsApp
class Application < Rails::Application
config.root = File.expand_path("../../", __FILE__)
config.active_record.legacy_connection_handling = false
end
end
8 changes: 8 additions & 0 deletions spec/rails_app/config/initializers/twirp_rails.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Rails.application.config.twirp.service_hooks[:before] = lambda do |rack_env, env|
# Make IP accessible to the handlers
env[:ip] = rack_env["REMOTE_ADDR"]
end

Rails.application.config.twirp.middleware = [
Rack::Deflater
]
6 changes: 6 additions & 0 deletions spec/rails_app/lib/ip_tracker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Realistic-looking class/model we'll interact with for testing.
class IPTracker
def self.track(ip)
@ip = ip
end
end
95 changes: 94 additions & 1 deletion spec/requests/haberdasher_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require "spec_helper"

RSpec.describe "Haberdasher Service", type: :request do
it "makes a hat" do
def make_hat_success_request
size = Twirp::Example::Haberdasher::Size.new(inches: 24)

post "/twirp/twirp.example.haberdasher.Haberdasher/MakeHat",
Expand All @@ -10,6 +10,12 @@
"Content-Type" => "application/protobuf"
}
expect(response).to be_successful
decoded = Twirp::Example::Haberdasher::Hat.decode(response.body)
expect(decoded).to be_a(Twirp::Example::Haberdasher::Hat)
end

it "makes a hat" do
make_hat_success_request
end

describe "error handling" do
Expand All @@ -26,5 +32,92 @@
expect(response.content_type).to eq("application/json")
expect(response.body).to eq('{"code":"invalid_argument","msg":"is too small","meta":{"argument":"inches"}}')
end

it "allows a before_action to return a Twirp::Error" do
size = Twirp::Example::Haberdasher::Size.new(inches: 12_000)

post "/twirp/twirp.example.haberdasher.Haberdasher/MakeHat",
params: size.to_proto, headers: {
:accept => "application/protobuf",
"Content-Type" => "application/protobuf"
}

expect(response.status).to eq(400)
expect(response.content_type).to eq("application/json")
expect(response.body).to eq('{"code":"invalid_argument","msg":"is too big","meta":{"argument":"inches"}}')
end
end

describe "notifications" do
before do
@events = []
@subscriber = ActiveSupport::Notifications.subscribe(/twirp_rails/) do |*args|
@events << ActiveSupport::Notifications::Event.new(*args)
end
end

after do
ActiveSupport::Notifications.unsubscribe(@subscriber)
end

it "publishes ActiveSupport::Notifications for the handler" do
make_hat_success_request

expect(@events).to contain_exactly(
have_attributes(name: "handler_run.twirp_rails",
payload: {
handler: "HaberdasherHandler",
action: "make_hat",
env: an_instance_of(Hash),
request: an_instance_of(Twirp::Example::Haberdasher::Size),
response: an_instance_of(Twirp::Example::Haberdasher::Hat)
}),
have_attributes(name: "handler_run_callbacks.twirp_rails",
payload: {
handler: "HaberdasherHandler",
action: "make_hat",
env: an_instance_of(Hash),
request: an_instance_of(Twirp::Example::Haberdasher::Size)
})
)

# In order that events were initialized
expect(@events.sort_by(&:time).map(&:name)).to contain_exactly("handler_run.twirp_rails", "handler_run_callbacks.twirp_rails")
end
end

describe "service hooks" do
it "can inject data via a before hook" do
expect(IPTracker).to receive(:track).with("127.0.0.1")
make_hat_success_request
end
end

describe "middleware" do
it "runs injected middleware when appropriate" do
size = Twirp::Example::Haberdasher::Size.new(inches: 24)

post "/twirp/twirp.example.haberdasher.Haberdasher/MakeHat",
params: size.to_proto, headers: {
:accept => "application/protobuf",
"Content-Type" => "application/protobuf",
"Accept-Encoding" => "gzip" # ask for GZIP encoding
}
expect(response.headers["Vary"]).to eq("Accept-Encoding")
expect(response.headers["Content-Encoding"]).to eq("gzip")
end

it "ignores injected middleware when inappropriate" do
size = Twirp::Example::Haberdasher::Size.new(inches: 24)

post "/twirp/twirp.example.haberdasher.Haberdasher/MakeHat",
params: size.to_proto, headers: {
:accept => "application/protobuf",
"Content-Type" => "application/protobuf"
# no encoding specified
}
expect(response.headers["Vary"]).to eq("Accept-Encoding")
expect(response.headers["Content-Encoding"]).to be_nil
end
end
end
2 changes: 1 addition & 1 deletion twirp-rails.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
spec.description = "A simple way to serve Twirp RPC services in a Rails app. Minimial configuration and familiar Rails conventions."
spec.homepage = "https://github.com/collectiveidea/twirp-rails"
spec.license = "MIT"
spec.required_ruby_version = ">= 2.6.0"
spec.required_ruby_version = ">= 2.7.0"

spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://github.com/collectiveidea/twirp-rails"
Expand Down

0 comments on commit 5323f52

Please sign in to comment.