Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions v2/ruby/lib/terminalwire/v2/rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,28 @@ def self.dual_terminal(cli, v1: nil, v2: nil)
)
end

# The v2-DEFAULT endpoint: serve `cli` over v2, with no v1. Mount it the same way
# as dual_terminal:
#
# match "/terminal", to: Terminalwire::V2::Rails.terminal(MainTerminal),
# via: [:get, :connect]
#
# It returns a version endpoint (not the bare Rack): a connection advertising the
# `terminalwire.v2` subprotocol — and any connection that doesn't ask for another
# version — is served by the v2 server. The endpoint is the forward-compatible
# seam: a future v3 registers another handler here without changing the app's
# route. (A bare Rack handed to `match to:` drops streaming output in production;
# the endpoint, like dual_terminal's, is what Rails routing needs.)
def self.terminal(cli, verbose: nil, report: nil)
Terminalwire::V2::Server.dualize(cli)
v2 = Terminalwire::V2::Server::Rack.new(
cli,
verbose: verbose.nil? ? verbose?() : verbose,
report: report || self.report
)
VersionEndpoint.new(default: v2, by_subprotocol: { SUBPROTOCOL => v2 })
end

# In dev/test, show the full backtrace to the client (consider_all_requests_local,
# like v1). In production the client sees the generic message — but the real
# exception is still LOGGED + reported (below), never silently swallowed.
Expand Down Expand Up @@ -97,6 +119,23 @@ def call(env)
(protos.include?(SUBPROTOCOL) ? @v2 : @v1).call(env)
end
end

# Routes a WebSocket upgrade to a handler by the version subprotocol it advertises,
# falling back to `default` (v2) when none matches — the forward-compatible seam for
# registering future protocol versions. Same Rack-endpoint shape as Dispatcher, so
# Rails `match to:` hands the connection off correctly in production.
class VersionEndpoint
def initialize(default:, by_subprotocol: {})
@default = default
@by_subprotocol = by_subprotocol
end

def call(env)
protos = env["HTTP_SEC_WEBSOCKET_PROTOCOL"].to_s.split(/,\s*/)
handler = protos.lazy.filter_map { |proto| @by_subprotocol[proto] }.first || @default
handler.call(env)
end
end
end
end
end
55 changes: 55 additions & 0 deletions v2/ruby/spec/server/rails_terminal_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

require "spec_helper"
require "terminalwire/v2/rails"

RSpec.describe Terminalwire::V2::Rails do
describe Terminalwire::V2::Rails::VersionEndpoint do
let(:v2) { ->(env) { [:v2, env] } }
let(:v3) { ->(env) { [:v3, env] } }

subject(:endpoint) do
described_class.new(default: v2, by_subprotocol: { "terminalwire.v2" => v2, "terminalwire.v3" => v3 })
end

def env(protocols)
{ "HTTP_SEC_WEBSOCKET_PROTOCOL" => protocols }
end

it "routes an advertised version subprotocol to its handler" do
expect(endpoint.call(env("terminalwire.v2")).first).to eq :v2
expect(endpoint.call(env("terminalwire.v3")).first).to eq :v3
end

it "defaults (to v2) when no known version is advertised" do
expect(endpoint.call(env(nil)).first).to eq :v2
expect(endpoint.call(env("")).first).to eq :v2
expect(endpoint.call(env("ws, made-up")).first).to eq :v2
end

it "picks the first matching version among several offered" do
expect(endpoint.call(env("ws, terminalwire.v3")).first).to eq :v3
end
end

describe ".terminal" do
let(:cli) { Class.new(Thor) }

it "returns a v2-default VersionEndpoint" do
expect(described_class.terminal(cli)).to be_a(Terminalwire::V2::Rails::VersionEndpoint)
end

it "routes the v2 subprotocol — and anything unversioned — to a Server::Rack" do
endpoint = described_class.terminal(cli)
rack = endpoint.instance_variable_get(:@default)
expect(rack).to be_a(Terminalwire::V2::Server::Rack)
# the v2 subprotocol maps to the same Rack as the default
expect(endpoint.instance_variable_get(:@by_subprotocol)["terminalwire.v2"]).to be(rack)
end

it "dualizes the cli so it answers the v2 wire" do
described_class.terminal(cli)
expect(cli.include?(Terminalwire::V2::Server::DualThor)).to be(true)
end
end
end
Loading