rails_twirp 0.2.0 → 0.7.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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +21 -0
  3. data/.gitignore +1 -0
  4. data/.standard.yml +4 -0
  5. data/Gemfile +3 -3
  6. data/lib/rails_twirp.rb +2 -0
  7. data/lib/rails_twirp/base.rb +36 -11
  8. data/lib/rails_twirp/engine.rb +29 -9
  9. data/lib/rails_twirp/errors.rb +11 -0
  10. data/lib/rails_twirp/implicit_render.rb +11 -0
  11. data/lib/rails_twirp/instrumentation.rb +32 -0
  12. data/lib/rails_twirp/log_subscriber.rb +64 -0
  13. data/lib/rails_twirp/mapper.rb +83 -0
  14. data/lib/rails_twirp/render_pb.rb +18 -0
  15. data/lib/rails_twirp/rescue.rb +14 -0
  16. data/lib/rails_twirp/route_set.rb +7 -48
  17. data/lib/rails_twirp/testing/integration_test.rb +73 -10
  18. data/lib/rails_twirp/url_for.rb +17 -0
  19. data/lib/rails_twirp/version.rb +1 -1
  20. data/test/dummy/Rakefile +6 -0
  21. data/test/dummy/app/assets/config/manifest.js +2 -0
  22. data/test/dummy/app/assets/images/.keep +0 -0
  23. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  24. data/test/dummy/app/channels/application_cable/channel.rb +4 -0
  25. data/test/dummy/app/channels/application_cable/connection.rb +4 -0
  26. data/test/dummy/app/controllers/application_controller.rb +2 -0
  27. data/test/dummy/app/controllers/application_twirp_controller.rb +7 -0
  28. data/test/dummy/app/controllers/concerns/.keep +0 -0
  29. data/test/dummy/app/controllers/pings_controller.rb +40 -0
  30. data/test/dummy/app/controllers/testmod/nested/other_controller.rb +10 -0
  31. data/test/dummy/app/helpers/application_helper.rb +2 -0
  32. data/test/dummy/app/helpers/random_helper.rb +5 -0
  33. data/test/dummy/app/javascript/packs/application.js +15 -0
  34. data/test/dummy/app/jobs/application_job.rb +7 -0
  35. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  36. data/test/dummy/app/models/application_record.rb +3 -0
  37. data/test/dummy/app/models/concerns/.keep +0 -0
  38. data/test/dummy/app/views/layouts/application.html.erb +15 -0
  39. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  40. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  41. data/test/dummy/app/views/pings/ping_template.pb.pbbuilder +1 -0
  42. data/test/dummy/bin/generate +3 -0
  43. data/test/dummy/bin/rails +4 -0
  44. data/test/dummy/bin/rake +4 -0
  45. data/test/dummy/bin/setup +33 -0
  46. data/test/dummy/config.ru +6 -0
  47. data/test/dummy/config/application.rb +23 -0
  48. data/test/dummy/config/boot.rb +5 -0
  49. data/test/dummy/config/cable.yml +10 -0
  50. data/test/dummy/config/database.yml +25 -0
  51. data/test/dummy/config/environment.rb +5 -0
  52. data/test/dummy/config/environments/development.rb +76 -0
  53. data/test/dummy/config/environments/production.rb +120 -0
  54. data/test/dummy/config/environments/test.rb +59 -0
  55. data/test/dummy/config/initializers/application_controller_renderer.rb +8 -0
  56. data/test/dummy/config/initializers/assets.rb +12 -0
  57. data/test/dummy/config/initializers/backtrace_silencers.rb +8 -0
  58. data/test/dummy/config/initializers/content_security_policy.rb +28 -0
  59. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  60. data/test/dummy/config/initializers/filter_parameter_logging.rb +6 -0
  61. data/test/dummy/config/initializers/inflections.rb +16 -0
  62. data/test/dummy/config/initializers/mime_types.rb +4 -0
  63. data/test/dummy/config/initializers/permissions_policy.rb +11 -0
  64. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  65. data/test/dummy/config/locales/en.yml +33 -0
  66. data/test/dummy/config/puma.rb +43 -0
  67. data/test/dummy/config/routes.rb +3 -0
  68. data/test/dummy/config/storage.yml +34 -0
  69. data/test/dummy/config/twirp/routes.rb +21 -0
  70. data/test/dummy/lib/assets/.keep +0 -0
  71. data/test/dummy/log/.keep +0 -0
  72. data/test/dummy/proto/api.proto +24 -0
  73. data/test/dummy/proto/api_pb.rb +22 -0
  74. data/test/dummy/proto/api_twirp.rb +24 -0
  75. data/test/dummy/public/404.html +67 -0
  76. data/test/dummy/public/422.html +67 -0
  77. data/test/dummy/public/500.html +66 -0
  78. data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
  79. data/test/dummy/public/apple-touch-icon.png +0 -0
  80. data/test/dummy/public/favicon.ico +0 -0
  81. data/test/other_controller_test.rb +9 -0
  82. data/test/ping_controller_test.rb +64 -0
  83. data/test/test_helper.rb +10 -1
  84. metadata +138 -2
@@ -1,6 +1,10 @@
1
1
  # Most of this logic is stolen from Rails ActionDispatch::Routing::RouteSet
2
2
 
3
+ require "rails_twirp/mapper"
4
+
3
5
  module RailsTwirp
6
+ class UnknownRpcError < StandardError; end
7
+
4
8
  class RouteSet
5
9
  attr_reader :services
6
10
 
@@ -42,15 +46,16 @@ module RailsTwirp
42
46
  handler = Class.new
43
47
  @rpcs.each do |name, mapping|
44
48
  rpc_info = @service_class.rpcs[name]
49
+ raise UnknownRpcError, "Unknown RPC #{name} on #{@service_class.service_name} service" unless rpc_info
45
50
  method_name = rpc_info[:ruby_method]
46
51
 
47
52
  # Stolen from Rails in ActionDispatch::Request#controller_class_for
48
- controller_name = mapping.controller.underscore
49
- const_name = controller_name.camelize << "Controller"
50
53
  action_name = mapping.action
51
54
  response_class = rpc_info[:output_class]
52
55
 
53
56
  handler.define_method(method_name) do |req, env|
57
+ controller_name = mapping.controller.underscore
58
+ const_name = controller_name.camelize << "Controller"
54
59
  controller_class = ::ActiveSupport::Dependencies.constantize(const_name)
55
60
  controller_class.dispatch(action_name, req, response_class, env)
56
61
  end
@@ -63,51 +68,5 @@ module RailsTwirp
63
68
  service
64
69
  end
65
70
  end
66
-
67
- class Mapping
68
- attr_reader :controller, :action
69
-
70
- def initialize(to:)
71
- @controller, @action = split_to(to)
72
- end
73
-
74
- def to_s
75
- "#{controller}##{action}"
76
- end
77
-
78
- private
79
-
80
- # copied from Rails
81
- def split_to(to)
82
- if /#/.match?(to)
83
- to.split("#").map!(&:-@)
84
- else
85
- []
86
- end
87
- end
88
- end
89
-
90
- class ServiceMapper
91
- def initialize(service_route_set)
92
- @service_route_set = service_route_set
93
- end
94
-
95
- def rpc(name, to:)
96
- mapping = Mapping.new(to: to)
97
- @service_route_set.add_route(name, mapping)
98
- end
99
- end
100
-
101
- class Mapper
102
- def initialize(route_set)
103
- @route_set = route_set
104
- end
105
-
106
- def service(service_definition, &block)
107
- service_route_set = @route_set.services[service_definition]
108
- service_mapper = ServiceMapper.new(service_route_set)
109
- service_mapper.instance_exec(&block)
110
- end
111
- end
112
71
  end
113
72
  end
@@ -1,6 +1,13 @@
1
+ require "twirp/encoding"
2
+
1
3
  module RailsTwirp
2
4
  class IntegrationTest < ActiveSupport::TestCase
5
+ DEFAULT_HOST = "www.example.com"
6
+ Response = Struct.new(:status, :body, :headers)
7
+
3
8
  attr_reader :response, :request, :controller
9
+ attr_writer :mount_path
10
+ alias :mount_path! :mount_path=
4
11
 
5
12
  def initialize(name)
6
13
  super
@@ -8,9 +15,27 @@ module RailsTwirp
8
15
  @before_rpc = []
9
16
  end
10
17
 
18
+ def host
19
+ @host || DEFAULT_HOST
20
+ end
21
+ attr_writer :host
22
+ alias :host! :host=
23
+
24
+ def https?
25
+ @https
26
+ end
27
+
28
+ def https!(value = true)
29
+ @https = value
30
+ end
31
+
11
32
  def reset!
12
33
  @request = nil
13
34
  @response = nil
35
+ @host = nil
36
+ @host = nil
37
+ @https = false
38
+ @mount_path = "/twirp"
14
39
  end
15
40
 
16
41
  def before_rpc(&block)
@@ -19,25 +44,63 @@ module RailsTwirp
19
44
 
20
45
  def rpc(service, rpc, request, headers: nil)
21
46
  @request = request
22
- service = app.twirp.routes.services[service].to_service
23
-
24
- rack_env = {}
25
- http_request = ActionDispatch::Request.new(rack_env)
26
- http_request.headers.merge! headers if headers.present?
27
- env = {rack_env: rack_env}
28
47
 
48
+ env = build_rack_env(service, rpc, request, headers)
29
49
  @before_rpc.each do |hook|
30
50
  hook.call(env)
31
51
  end
32
52
 
33
- response = service.call_rpc rpc, request, env
34
- @response = response
35
- @controller = http_request.controller_instance
36
- response
53
+ status, headers, body = app.call(env)
54
+ @response = decode_rack_response(service, rpc, status, headers, body)
55
+ set_controller_from_rack_env(env)
56
+
57
+ @response
37
58
  end
38
59
 
39
60
  def app
40
61
  RailsTwirp.test_app
41
62
  end
63
+
64
+ private
65
+
66
+ def build_rack_env(service, rpc, request, headers)
67
+ env = {
68
+ "CONTENT_TYPE" => request_content_type,
69
+ "HTTPS" => https? ? "on" : "off",
70
+ "HTTP_HOST" => host,
71
+ "PATH_INFO" => "#{@mount_path}/#{service.service_full_name}/#{rpc}",
72
+ "REQUEST_METHOD" => "POST",
73
+ "SERVER_NAME" => host,
74
+ "SERVER_PORT" => https? ? "443" : "80",
75
+ "rack.url_scheme" => https? ? "https" : "http"
76
+ }
77
+ if headers.present?
78
+ http_request = ActionDispatch::Request.new(env)
79
+ http_request.headers.merge! headers
80
+ end
81
+
82
+ input_class = service.rpcs[rpc][:input_class]
83
+ env["rack.input"] = StringIO.new(Twirp::Encoding.encode(request, input_class, request_content_type))
84
+ env
85
+ end
86
+
87
+ def request_content_type
88
+ Twirp::Encoding::PROTO
89
+ end
90
+
91
+ def decode_rack_response(service, rpc, status, headers, body)
92
+ body = body.join # body is an Enumerable
93
+
94
+ if status === 200
95
+ output_class = service.rpcs[rpc][:output_class]
96
+ Twirp::Encoding.decode(body, output_class, headers["Content-Type"])
97
+ else
98
+ Twirp::Client.error_from_response(Response.new(status, body, headers))
99
+ end
100
+ end
101
+
102
+ def set_controller_from_rack_env(env)
103
+ @controller = ActionDispatch::Request.new(env).controller_class
104
+ end
42
105
  end
43
106
  end
@@ -0,0 +1,17 @@
1
+ require "abstract_controller/url_for"
2
+
3
+ module RailsTwirp
4
+ module UrlFor
5
+ extend ActiveSupport::Concern
6
+
7
+ include AbstractController::UrlFor
8
+
9
+ def url_options
10
+ @_url_options ||= {
11
+ host: http_request.host,
12
+ port: http_request.optional_port,
13
+ protocol: http_request.protocol
14
+ }.merge!(super).freeze
15
+ end
16
+ end
17
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsTwirp
2
- VERSION = "0.2.0"
2
+ VERSION = "0.7.0"
3
3
  end
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require_relative "config/application"
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,2 @@
1
+ //= link_tree ../images
2
+ //= link_directory ../stylesheets .css
File without changes
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,4 @@
1
+ module ApplicationCable
2
+ class Channel < ActionCable::Channel::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module ApplicationCable
2
+ class Connection < ActionCable::Connection::Base
3
+ end
4
+ end
@@ -0,0 +1,2 @@
1
+ class ApplicationController < ActionController::Base
2
+ end
@@ -0,0 +1,7 @@
1
+ class ApplicationTwirpController < RailsTwirp::Base
2
+ rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
3
+
4
+ def handle_not_found
5
+ error :not_found, "Not found"
6
+ end
7
+ end
File without changes
@@ -0,0 +1,40 @@
1
+ class PingsController < ApplicationTwirpController
2
+ before_action :respond_error, only: :before_error
3
+
4
+ def ping
5
+ response = RPC::DummyAPI::PingResponse.new(double_name: request.name * 2)
6
+ self.response_body = response
7
+ end
8
+
9
+ def ping_render
10
+ url = rails_twirp_engine_url
11
+ response = RPC::DummyAPI::PingResponse.new(double_name: "#{url} #{helpers.does_this_work(request.name)}")
12
+ render pb: response
13
+ end
14
+
15
+ def ping_template
16
+ @double_name = request.name * 2
17
+ end
18
+
19
+ def error_response
20
+ error :unauthenticated, "You are not authenticated!!"
21
+ end
22
+
23
+ def raise_error
24
+ # This error is rescued in ApplicationTwirpController
25
+ raise ActiveRecord::RecordNotFound, "Not found"
26
+ end
27
+
28
+ def uncaught_raise
29
+ raise StandardError, "Uncaught"
30
+ end
31
+
32
+ def before_error
33
+ # This error won't be reached because of the before_action
34
+ raise NotImplementedError
35
+ end
36
+
37
+ def respond_error
38
+ error :malformed, "yOuR ReQuEsT Is mAlFoRmEd"
39
+ end
40
+ end
@@ -0,0 +1,10 @@
1
+ module Testmod
2
+ module Nested
3
+ class OtherController < ApplicationTwirpController
4
+ def ping
5
+ response = RPC::DummyAPI::PingResponse.new(double_name: request.name * 2)
6
+ self.response_body = response
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,5 @@
1
+ module RandomHelper
2
+ def does_this_work(n)
3
+ n * 2
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file. JavaScript code in this file should be added after the last require_* statement.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require rails-ujs
14
+ //= require activestorage
15
+ //= require_tree .
@@ -0,0 +1,7 @@
1
+ class ApplicationJob < ActiveJob::Base
2
+ # Automatically retry jobs that encountered a deadlock
3
+ # retry_on ActiveRecord::Deadlocked
4
+
5
+ # Most jobs are safe to ignore if the underlying records are no longer available
6
+ # discard_on ActiveJob::DeserializationError
7
+ end
@@ -0,0 +1,4 @@
1
+ class ApplicationMailer < ActionMailer::Base
2
+ default from: "from@example.com"
3
+ layout "mailer"
4
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ self.abstract_class = true
3
+ end
File without changes
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Dummy</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+
9
+ <%= stylesheet_link_tag 'application', media: 'all' %>
10
+ </head>
11
+
12
+ <body>
13
+ <%= yield %>
14
+ </body>
15
+ </html>
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
5
+ <style>
6
+ /* Email styles need to be inline */
7
+ </style>
8
+ </head>
9
+
10
+ <body>
11
+ <%= yield %>
12
+ </body>
13
+ </html>
@@ -0,0 +1 @@
1
+ <%= yield %>
@@ -0,0 +1 @@
1
+ pb.double_name @double_name
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+
3
+ protoc --twirp_ruby_out=proto/ --ruby_out=proto/ -I proto/ proto/api.proto
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ APP_PATH = File.expand_path("../config/application", __dir__)
3
+ require_relative "../config/boot"
4
+ require "rails/commands"
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative "../config/boot"
3
+ require "rake"
4
+ Rake.application.run
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+ require "fileutils"
3
+
4
+ # path to your application root.
5
+ APP_ROOT = File.expand_path("..", __dir__)
6
+
7
+ def system!(*args)
8
+ system(*args) || abort("\n== Command #{args} failed ==")
9
+ end
10
+
11
+ FileUtils.chdir APP_ROOT do
12
+ # This script is a way to set up or update your development environment automatically.
13
+ # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14
+ # Add necessary setup steps to this file.
15
+
16
+ puts "== Installing dependencies =="
17
+ system! "gem install bundler --conservative"
18
+ system("bundle check") || system!("bundle install")
19
+
20
+ # puts "\n== Copying sample files =="
21
+ # unless File.exist?('config/database.yml')
22
+ # FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
23
+ # end
24
+
25
+ puts "\n== Preparing database =="
26
+ system! "bin/rails db:prepare"
27
+
28
+ puts "\n== Removing old logs and tempfiles =="
29
+ system! "bin/rails log:clear tmp:clear"
30
+
31
+ puts "\n== Restarting application server =="
32
+ system! "bin/rails restart"
33
+ end