twirp-on-rails 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.standard.yml +4 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +15 -0
- data/LICENSE.txt +21 -0
- data/README.md +173 -0
- data/Rakefile +10 -0
- data/config/routes.rb +11 -0
- data/lib/twirp/on/rails.rb +3 -0
- data/lib/twirp/rails/callbacks.rb +55 -0
- data/lib/twirp/rails/configuration.rb +33 -0
- data/lib/twirp/rails/dispatcher.rb +21 -0
- data/lib/twirp/rails/engine.rb +64 -0
- data/lib/twirp/rails/handler.rb +51 -0
- data/lib/twirp/rails/rack/conditional_post.rb +51 -0
- data/lib/twirp/rails/version.rb +7 -0
- data/lib/twirp/rails.rb +30 -0
- data/sig/twirp/rails.rbs +6 -0
- data/twirp-rails.gemspec +34 -0
- metadata +95 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1bb31fa12f51069d7d6e447901646dd1ebfdc657d59bc670fffc57f8f858b34e
|
4
|
+
data.tar.gz: 414f84e6b7b655fabad364721fc6ea31ee94300674303ff633b5bf49434610cd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 986ee79625c2cea727ebbb09b0efb0f110a5e64f3ce5d186dfd2786ceb35d48f88935a43335f4c6740144fa6023cc12107288554d2a3389f37bf3f6ad2b01f21
|
7
|
+
data.tar.gz: 68c86eb0918f123e6b96457944347893f96bdfbabe91c4c3f1bd8b0ae75c6eac3b590856f6156c3488ade4a58ebcc906dc566b8c255326da3eb89344796e4a5f
|
data/.rspec
ADDED
data/.standard.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in twirp-rails.gemspec
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
gem "rake"
|
9
|
+
|
10
|
+
gem "debug"
|
11
|
+
gem "rspec-rails"
|
12
|
+
gem "sqlite3", "~> 1.4"
|
13
|
+
gem "standard"
|
14
|
+
gem "standard-performance"
|
15
|
+
gem "standard-rails"
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2022–2023 Collective Idea
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
[![CI](https://github.com/collectiveidea/twirp-rails/actions/workflows/ci.yml/badge.svg)](https://github.com/collectiveidea/twirp-rails/actions/workflows/ci.yml)
|
2
|
+
[![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
|
3
|
+
|
4
|
+
# Twirp on Rails (Twirp::Rails)
|
5
|
+
|
6
|
+
## Motivation
|
7
|
+
|
8
|
+
Make serving [Twirp](https://twitchtv.github.io/twirp/) RPC Services as easy and familiar as Rails controllers. Add a few helpful abstractions, but don't hide [Twirp](https://twitchtv.github.io/twirp/), [Protobufs](https://protobuf.dev), or make it seem too magical.
|
9
|
+
|
10
|
+
Out of the box, the [`twirp` gem](http://github.com/github/twirp-ruby) makes it easy to add [Services](https://github.com/github/twirp-ruby/wiki/Service-Handlers), but it feels clunky coming from Rails REST-ful APIs. We make it simple to build full-featured APIs. Hook in authorization, use `before_action` and more.
|
11
|
+
|
12
|
+
Extracted from a real, production application with many thousands of users.
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Install the gem and add to the application's Gemfile by executing:
|
17
|
+
|
18
|
+
$ bundle add twirp-on-rails
|
19
|
+
|
20
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
21
|
+
|
22
|
+
$ gem install twirp-on-rails
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
Add to your `routes.rb`:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
mount Twirp::Rails::Engine, at: "/twirp"
|
30
|
+
```
|
31
|
+
|
32
|
+
### Configuration
|
33
|
+
|
34
|
+
Twirp::Rails will automatically load any `*_twirp.rb` files in your app's `lib/` directory (and subdirectories). To modify the location, add this to an initializer:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
Rails.application.config.load_paths = ["lib", "app/twirp"]
|
38
|
+
```
|
39
|
+
|
40
|
+
## Features
|
41
|
+
|
42
|
+
### Easy Routing
|
43
|
+
|
44
|
+
Add one line to your `config/routes.rb` and routes are built automatically from your Twirp Services:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
mount Twirp::Rails::Engine, at: "/twirp"
|
48
|
+
```
|
49
|
+
|
50
|
+
`/twirp/twirp.example.haberdasher.HaberdasherService/MakeHat`
|
51
|
+
|
52
|
+
These are routed to Handlers in `app/handlers/` based on expected naming conventions.
|
53
|
+
|
54
|
+
For example if you have this service defined:
|
55
|
+
|
56
|
+
```protobuf
|
57
|
+
package twirp.example.haberdasher;
|
58
|
+
|
59
|
+
service HaberdasherService {
|
60
|
+
rpc MakeHat(Size) returns (Hat);
|
61
|
+
}
|
62
|
+
```
|
63
|
+
|
64
|
+
it will expect to find `app/handlers/haberdasher_service_handler.rb` with a `make_hat` method.
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
class HaberdasherServiceHandler < Twirp::Rails::Handler
|
68
|
+
def make_hat
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
Each handler method should return the appropriate Protobuf, or a `Twirp::Error`.
|
75
|
+
|
76
|
+
TODO: Give more examples of both
|
77
|
+
|
78
|
+
### Familiar Callbacks
|
79
|
+
|
80
|
+
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).
|
81
|
+
|
82
|
+
### DRY Service Hooks
|
83
|
+
|
84
|
+
Apply [Service Hooks](https://github.com/twitchtv/twirp-ruby/wiki/Service-Hooks) one time across multiple services.
|
85
|
+
|
86
|
+
For example, we can add hooks in an initializer:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
# Make IP address accessible to the handlers
|
90
|
+
Rails.application.config.twirp.service_hooks[:before] = lambda do |rack_env, env|
|
91
|
+
env[:ip] = rack_env["REMOTE_ADDR"]
|
92
|
+
end
|
93
|
+
|
94
|
+
# Send exceptions to Honeybadger
|
95
|
+
Rails.application.config.twirp.service_hooks[:exception_raised] = ->(exception, _env) { Honeybadger.notify(exception) }
|
96
|
+
```
|
97
|
+
|
98
|
+
### Middleware
|
99
|
+
|
100
|
+
As an Engine, we avoid all the standard Rails middleware. That's nice for simplicity, but sometimes you want to add your own middleware. You can do that by specifying it in an initializer:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
Rails.application.config.twirp.middleware = [Rack::Deflater]
|
104
|
+
```
|
105
|
+
|
106
|
+
## Bonus Features
|
107
|
+
|
108
|
+
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.
|
109
|
+
|
110
|
+
### Basic Caching with ETags/If-None-Match Headers
|
111
|
+
|
112
|
+
Like Rails GET actions, Twirp::Rails handlers add [`ETag` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) based on the response's content.
|
113
|
+
|
114
|
+
If you have RPCs that can be cached, you can have your Twirp clients send an [`If-None-Match` Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match). Twirp::Rails will return a `304 Not Modified` HTTP status and not re-send the body if the ETag matches.
|
115
|
+
|
116
|
+
Enable by adding this to an initializer:
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
Rails.application.config.twirp.middleware = [
|
120
|
+
Twirp::Rails::Rack::ConditionalPost,
|
121
|
+
Rack::ETag
|
122
|
+
]
|
123
|
+
```
|
124
|
+
|
125
|
+
## TODO
|
126
|
+
|
127
|
+
* More docs!
|
128
|
+
* More tests!
|
129
|
+
* installer generator to add `ApplicationHandler`
|
130
|
+
* Maybe a generator for individual handlers that adds that if needed?
|
131
|
+
* Auto reload.
|
132
|
+
* Make service hooks more configurable? Apply to one service instead of all?
|
133
|
+
* Loosen Rails version requirement? Probably works, but haven't tested.
|
134
|
+
|
135
|
+
## Prior Art
|
136
|
+
|
137
|
+
We evaluated all these projects and found them to be bad fits for us, for one reason or another. We're grateful to all for their work, and hope they continue and flourish. Some notes from our initial evaluation:
|
138
|
+
|
139
|
+
[nikushi/twirp-rails](https://github.com/nikushi/twirp-rails)
|
140
|
+
|
141
|
+
* Nice routing abstraction
|
142
|
+
* Minimal Handler abstraction
|
143
|
+
* Untouched for 4 years
|
144
|
+
|
145
|
+
[cheddar-me/rails-twirp](https://github.com/cheddar-me/rails-twirp)
|
146
|
+
|
147
|
+
* Too much setup.
|
148
|
+
* Nice controllers, but expects you to use their [pbbuilder](https://github.com/cheddar-me/pbbuilder) which I find unnecessary.
|
149
|
+
|
150
|
+
[severgroup-tt/twirp_rails-1](https://github.com/severgroup-tt/twirp_rails-1)
|
151
|
+
|
152
|
+
* Some nice things
|
153
|
+
* No Handler abstractions
|
154
|
+
* Archived and not touched for 3 years
|
155
|
+
|
156
|
+
[dudo/rails_respond_to_pb](https://github.com/dudo/rails_respond_to_pb)
|
157
|
+
|
158
|
+
* Allows routing to existing controllers
|
159
|
+
* I dislike the `respond_to` stuff. That shouldn't be something you think about. We have a better way to do that in other recent apps anyway.
|
160
|
+
|
161
|
+
## Contributing
|
162
|
+
|
163
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/danielmorrison/twirp-rails.
|
164
|
+
|
165
|
+
## Development
|
166
|
+
|
167
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
168
|
+
|
169
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
170
|
+
|
171
|
+
## License
|
172
|
+
|
173
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/config/routes.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
Rails.application.routes.draw do
|
2
|
+
if Rails.application.config.twirp.auto_mount
|
3
|
+
mount Twirp::Rails::Engine => Rails.application.config.twirp.endpoint
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
Twirp::Rails::Engine.routes.draw do
|
8
|
+
Twirp::Rails.services.each do |service|
|
9
|
+
mount service, at: service.full_name
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
require "active_support/callbacks"
|
5
|
+
require "abstract_controller/callbacks"
|
6
|
+
|
7
|
+
module Twirp
|
8
|
+
module Rails
|
9
|
+
# = Twirp Rails Callbacks
|
10
|
+
#
|
11
|
+
# Based on Abstract Controller Callbacks with the terminator changed.
|
12
|
+
#
|
13
|
+
# A before_action that returns a Twirp::Error will halt the action.
|
14
|
+
#
|
15
|
+
# Abstract Controller provides hooks during the life cycle of a controller action.
|
16
|
+
# Callbacks allow you to trigger logic during this cycle. Available callbacks are:
|
17
|
+
#
|
18
|
+
# * <tt>after_action</tt>
|
19
|
+
# * <tt>append_after_action</tt>
|
20
|
+
# * <tt>append_around_action</tt>
|
21
|
+
# * <tt>append_before_action</tt>
|
22
|
+
# * <tt>around_action</tt>
|
23
|
+
# * <tt>before_action</tt>
|
24
|
+
# * <tt>prepend_after_action</tt>
|
25
|
+
# * <tt>prepend_around_action</tt>
|
26
|
+
# * <tt>prepend_before_action</tt>
|
27
|
+
# * <tt>skip_after_action</tt>
|
28
|
+
# * <tt>skip_around_action</tt>
|
29
|
+
# * <tt>skip_before_action</tt>
|
30
|
+
#
|
31
|
+
# NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
|
32
|
+
#
|
33
|
+
module Callbacks
|
34
|
+
extend ActiveSupport::Concern
|
35
|
+
|
36
|
+
include AbstractController::Callbacks
|
37
|
+
|
38
|
+
included do
|
39
|
+
define_callbacks :process_action,
|
40
|
+
terminator: ->(controller, result_lambda) {
|
41
|
+
# save off the error and terminate if a callback returns a Twirp::Error
|
42
|
+
result = result_lambda.call
|
43
|
+
|
44
|
+
if result.is_a?(Twirp::Error)
|
45
|
+
controller.error = result
|
46
|
+
true
|
47
|
+
else
|
48
|
+
false
|
49
|
+
end
|
50
|
+
},
|
51
|
+
skip_after_callbacks_if_terminated: true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Twirp
|
4
|
+
module Rails
|
5
|
+
class Configuration
|
6
|
+
# Whether to automatically mount routes at endpoint. Defaults to false
|
7
|
+
attr_accessor :auto_mount
|
8
|
+
|
9
|
+
# Where to mount twirp routes. Defaults to /twirp
|
10
|
+
attr_accessor :endpoint
|
11
|
+
|
12
|
+
# An array of directories to search for *_twirp.rb files
|
13
|
+
# Defaults to ["lib"]
|
14
|
+
attr_accessor :load_paths
|
15
|
+
|
16
|
+
# An array of Rack middleware to use
|
17
|
+
attr_accessor :middleware
|
18
|
+
|
19
|
+
# A hash of lambdas that accepts |rack_env, env| and is passed to Twirp::Service
|
20
|
+
# See: https://github.com/twitchtv/twirp-ruby/wiki/Service-Hooks
|
21
|
+
# for available hooks
|
22
|
+
attr_accessor :service_hooks
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@auto_mount = false
|
26
|
+
@endpoint = "/twirp"
|
27
|
+
@load_paths = ["lib"]
|
28
|
+
@middleware = []
|
29
|
+
@service_hooks = {}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Twirp
|
4
|
+
module Rails
|
5
|
+
class Dispatcher
|
6
|
+
def initialize(service_class)
|
7
|
+
@service_handler = "#{service_class.service_name}Handler".constantize
|
8
|
+
end
|
9
|
+
|
10
|
+
def respond_to_missing?(method, *)
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
14
|
+
def method_missing(name, *args)
|
15
|
+
request = args[0]
|
16
|
+
env = args[1]
|
17
|
+
@service_handler.new.process(name, request, env)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/engine"
|
4
|
+
require "rack/etag"
|
5
|
+
require_relative "rack/conditional_post"
|
6
|
+
|
7
|
+
module Twirp
|
8
|
+
module Rails
|
9
|
+
class Engine < ::Rails::Engine
|
10
|
+
isolate_namespace Twirp::Rails
|
11
|
+
engine_name "twirp"
|
12
|
+
# endpoint MyRackApplication
|
13
|
+
# # Add a load path for this specific Engine
|
14
|
+
# config.autoload_paths << File.expand_path("lib/some/path", __dir__)
|
15
|
+
|
16
|
+
config.twirp = Configuration.new
|
17
|
+
|
18
|
+
initializer "twirp.configure.defaults", before: "twirp.configure" do |app|
|
19
|
+
twirp = app.config.twirp
|
20
|
+
# twirp.auto_mount = true if twirp.auto_mount.nil?
|
21
|
+
twirp.load_paths ||= ["lib"]
|
22
|
+
end
|
23
|
+
|
24
|
+
initializer "twirp.configure" do |app|
|
25
|
+
[:auto_mount, :endpoint, :load_paths, :middleware, :service_hooks].each do |key|
|
26
|
+
app.config.twirp.send(key)
|
27
|
+
end
|
28
|
+
|
29
|
+
app.config.twirp.middleware.each do |middleware|
|
30
|
+
app.config.middleware.use middleware
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def services
|
37
|
+
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
|
+
@services = Twirp::Service.subclasses.map(&:new)
|
43
|
+
|
44
|
+
# Install hooks that may be defined in the config
|
45
|
+
@services.each do |service|
|
46
|
+
::Rails.application.config.twirp.service_hooks.each do |hook_name, hook|
|
47
|
+
service.send(hook_name, &hook)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
@services
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
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,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Twirp
|
4
|
+
module Rails
|
5
|
+
class Handler
|
6
|
+
include Twirp::Rails::Callbacks
|
7
|
+
|
8
|
+
attr_reader :request, :env
|
9
|
+
attr_reader :action_name
|
10
|
+
attr_accessor :error
|
11
|
+
|
12
|
+
# @param name [Symbol] The method name to invoke in the handler
|
13
|
+
# @param request [Object] The protobuf message request parameter
|
14
|
+
# @param env [Hash] The Twirp environment
|
15
|
+
def process(name, request, env)
|
16
|
+
@request = request
|
17
|
+
@env = env
|
18
|
+
@error = nil
|
19
|
+
@action_name = name.to_s
|
20
|
+
|
21
|
+
response = process_action(action_name)
|
22
|
+
error || response
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# Call the action. Override this in a subclass to modify the
|
28
|
+
# behavior around processing an action. This, and not #process,
|
29
|
+
# is the intended way to override action dispatching.
|
30
|
+
#
|
31
|
+
# Notice that the first argument is the method to be dispatched
|
32
|
+
# which is *not* necessarily the same as the action name.
|
33
|
+
def process_action(name)
|
34
|
+
ActiveSupport::Notifications.instrument("handler_run_callbacks.twirp_rails", handler: self.class.name, action: action_name, env: @env, request: @request) do
|
35
|
+
run_callbacks(:process_action) do
|
36
|
+
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)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Actually call the method associated with the action. Override
|
44
|
+
# this method if you wish to change how action methods are called,
|
45
|
+
# not to add additional behavior around it. For example, you would
|
46
|
+
# override #send_action if you want to inject arguments into the
|
47
|
+
# method.
|
48
|
+
alias_method :send_action, :send
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack/conditional_get"
|
4
|
+
|
5
|
+
module Twirp
|
6
|
+
module Rails
|
7
|
+
module Rack
|
8
|
+
# Middleware that enables conditional POST using If-None-Match and
|
9
|
+
# If-Modified-Since. The application should set either or both of the
|
10
|
+
# Last-Modified or Etag response headers according to RFC 2616. When
|
11
|
+
# either of the conditions is met, the response body is set to be zero
|
12
|
+
# length and the response status is set to 304 Not Modified.
|
13
|
+
#
|
14
|
+
# Applications that defer response body generation until the body's each
|
15
|
+
# message is received will avoid response body generation completely when
|
16
|
+
# a conditional POST matches.
|
17
|
+
#
|
18
|
+
# Based on Rack::ConditionalGet
|
19
|
+
#
|
20
|
+
# Twirp requests are, be design, always POST.
|
21
|
+
# We want the logic of Rack::ConditionalGet but applied to POSTs.
|
22
|
+
#
|
23
|
+
# Not all Twirp calls are idemtpotent, so it is left up to the client
|
24
|
+
# to know when 304 Not Modified responses are desirable, and send the
|
25
|
+
# appropriate header(s).
|
26
|
+
class ConditionalPost < ::Rack::ConditionalGet
|
27
|
+
# Return empty 304 response if the response has not been
|
28
|
+
# modified since the last request.
|
29
|
+
def call(env)
|
30
|
+
case env[::Rack::REQUEST_METHOD]
|
31
|
+
when "POST"
|
32
|
+
status, headers, body = @app.call(env)
|
33
|
+
headers = ::Rack::Utils::HeaderHash[headers]
|
34
|
+
if status == 200 && fresh?(env, headers)
|
35
|
+
status = 304
|
36
|
+
headers.delete(::Rack::CONTENT_TYPE)
|
37
|
+
headers.delete(::Rack::CONTENT_LENGTH)
|
38
|
+
original_body = body
|
39
|
+
body = ::Rack::BodyProxy.new([]) do
|
40
|
+
original_body.close if original_body.respond_to?(:close)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
[status, headers, body]
|
44
|
+
else
|
45
|
+
@app.call(env)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/twirp/rails.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "rails/version"
|
4
|
+
|
5
|
+
module Twirp
|
6
|
+
module Rails
|
7
|
+
class Error < StandardError; end
|
8
|
+
# Your code goes here...
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
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"
|
19
|
+
|
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
|
29
|
+
end
|
30
|
+
end
|
data/sig/twirp/rails.rbs
ADDED
data/twirp-rails.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/twirp/rails/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "twirp-on-rails"
|
7
|
+
spec.version = Twirp::Rails::VERSION
|
8
|
+
spec.authors = ["Daniel Morrison", "Darron Schall"]
|
9
|
+
spec.email = ["info@collectiveidea.com"]
|
10
|
+
|
11
|
+
spec.summary = "Use Twirp RPC with Rails"
|
12
|
+
spec.description = "A simple way to serve Twirp RPC services in a Rails app. Minimial configuration and familiar Rails conventions."
|
13
|
+
spec.homepage = "https://github.com/collectiveidea/twirp-rails"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 2.7.0"
|
16
|
+
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/collectiveidea/twirp-rails"
|
19
|
+
spec.metadata["changelog_uri"] = "https://github.com/collectiveidea/twirp-rails/blob/main/CHANGELOG.md"
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
26
|
+
end
|
27
|
+
end
|
28
|
+
spec.bindir = "exe"
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ["lib"]
|
31
|
+
|
32
|
+
spec.add_dependency "rails", ">= 7.0.0"
|
33
|
+
spec.add_dependency "twirp", ">= 1.8.0"
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: twirp-on-rails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Daniel Morrison
|
8
|
+
- Darron Schall
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2024-05-08 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 7.0.0
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 7.0.0
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: twirp
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 1.8.0
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 1.8.0
|
42
|
+
description: A simple way to serve Twirp RPC services in a Rails app. Minimial configuration
|
43
|
+
and familiar Rails conventions.
|
44
|
+
email:
|
45
|
+
- info@collectiveidea.com
|
46
|
+
executables: []
|
47
|
+
extensions: []
|
48
|
+
extra_rdoc_files: []
|
49
|
+
files:
|
50
|
+
- ".rspec"
|
51
|
+
- ".standard.yml"
|
52
|
+
- CHANGELOG.md
|
53
|
+
- Gemfile
|
54
|
+
- LICENSE.txt
|
55
|
+
- README.md
|
56
|
+
- Rakefile
|
57
|
+
- config/routes.rb
|
58
|
+
- lib/twirp/on/rails.rb
|
59
|
+
- lib/twirp/rails.rb
|
60
|
+
- lib/twirp/rails/callbacks.rb
|
61
|
+
- lib/twirp/rails/configuration.rb
|
62
|
+
- lib/twirp/rails/dispatcher.rb
|
63
|
+
- lib/twirp/rails/engine.rb
|
64
|
+
- lib/twirp/rails/handler.rb
|
65
|
+
- lib/twirp/rails/rack/conditional_post.rb
|
66
|
+
- lib/twirp/rails/version.rb
|
67
|
+
- sig/twirp/rails.rbs
|
68
|
+
- twirp-rails.gemspec
|
69
|
+
homepage: https://github.com/collectiveidea/twirp-rails
|
70
|
+
licenses:
|
71
|
+
- MIT
|
72
|
+
metadata:
|
73
|
+
homepage_uri: https://github.com/collectiveidea/twirp-rails
|
74
|
+
source_code_uri: https://github.com/collectiveidea/twirp-rails
|
75
|
+
changelog_uri: https://github.com/collectiveidea/twirp-rails/blob/main/CHANGELOG.md
|
76
|
+
post_install_message:
|
77
|
+
rdoc_options: []
|
78
|
+
require_paths:
|
79
|
+
- lib
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: 2.7.0
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
requirements: []
|
91
|
+
rubygems_version: 3.5.9
|
92
|
+
signing_key:
|
93
|
+
specification_version: 4
|
94
|
+
summary: Use Twirp RPC with Rails
|
95
|
+
test_files: []
|