diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 203c2b4..56238ce 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,9 +21,11 @@ jobs: rspec: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: activesupport: ['6.1', '7.0'] - ruby: ['2.7', '3.0', '3.1', '3.2'] + rack: ['2.2', '3.0'] + ruby: ['2.7', '3.1', '3.2'] steps: - uses: actions/checkout@v2 - name: Set up Ruby @@ -34,5 +36,6 @@ jobs: cache-version: ${{ matrix.activesupport }} env: ACTIVESUPPORT: ${{ matrix.activesupport }} + RACK: ${{ matrix.rack }} - name: Rspec run: bundle exec rspec diff --git a/Gemfile b/Gemfile index b414614..473a3b6 100644 --- a/Gemfile +++ b/Gemfile @@ -9,3 +9,7 @@ end if (version = ENV['ACTIVESUPPORT']) gem 'activesupport', "~> #{version}.0" end + +if (version = ENV['RACK']) + gem 'rack', "~> #{version}.0" +end diff --git a/api_valve.gemspec b/api_valve.gemspec index a34c1c8..db54c92 100644 --- a/api_valve.gemspec +++ b/api_valve.gemspec @@ -16,10 +16,11 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', '>= 6.1', '< 7.1' s.add_dependency 'faraday', '>= 0.14', '<= 2.5.2' s.add_dependency 'multi_json', '~> 1.13' - s.add_dependency 'rack', '~> 2' + s.add_dependency 'rack', '>= 2', '< 4' s.add_development_dependency 'json_spec', '~> 1.1' s.add_development_dependency 'rack-test', '~> 2.0' + s.add_development_dependency 'rackup' s.add_development_dependency 'rspec', '~> 3.7' s.add_development_dependency 'rubocop', '1.36.0' s.add_development_dependency 'rubocop-rspec', '2.13.1' diff --git a/examples/aggregation/config.ru b/examples/aggregation/config.ru index 2474711..443fe1d 100644 --- a/examples/aggregation/config.ru +++ b/examples/aggregation/config.ru @@ -1,4 +1,5 @@ require 'api_valve' +require 'byebug' app = Rack::Builder.new do use ApiValve::Middleware::ErrorHandling @@ -9,11 +10,10 @@ app = Rack::Builder.new do threads = (1..5).map do |i| Thread.new { forwarder.call request, 'path' => "posts/#{i}" } end - threads.each(&:join) body = threads.map(&:value).map do |rack_response| - JSON.parse(rack_response[2].first) + JSON.parse(rack_response.body.first) end.to_json - [200, {'Content-Type' => 'application/json'}, [body]] + Rack::Response.new(body, 200, {'Content-Type' => 'application/json'}) end end diff --git a/examples/basic/README.md b/examples/basic/README.md index e79413b..d1c7a2f 100644 --- a/examples/basic/README.md +++ b/examples/basic/README.md @@ -8,9 +8,6 @@ rackup -q -p 8080 As you can see from the `config.ru`, it will forward all requests according to this table: -| Route | Target | -|----------------|-------------------------------| -| /oauth/ | http://auth.host/oauth/ | -| /oauth/token | http://auth.host/oauth/token | -| /api/ | http://api.host/api/ | -| /api/something | http://api.host/api/something | +| Route | Target | +|----------------|------------------------------------------------| +| /api/* | http://jsonplaceholder.typicode.com/* | diff --git a/examples/basic/config.ru b/examples/basic/config.ru index f370a96..63f1dac 100644 --- a/examples/basic/config.ru +++ b/examples/basic/config.ru @@ -2,10 +2,10 @@ require 'api_valve' app = Rack::Builder.new do map '/api' do - run ApiValve::Proxy.from_hash(endpoint: 'http://api.host/api/') + run ApiValve::Proxy.from_hash(endpoint: 'https://jsonplaceholder.typicode.com') end - map '/oauth' do - run ApiValve::Proxy.from_hash(endpoint: 'http://auth.host/oauth/') + map '/health' do + run ->(_env) { [200, {}, ['']] } end end diff --git a/examples/routing/README.md b/examples/routing/README.md index dbf303b..93e351f 100644 --- a/examples/routing/README.md +++ b/examples/routing/README.md @@ -8,10 +8,10 @@ rackup -q -p 8080 As you can see from the `config.ru`, it will forward all requests according to this table: -| Method | Route | Target | -|--------|-----------------------|-----------------------| -| GET | /api/* | http://api.host/api/* | -| GET | /api/prefix/* | http://api.host/api/* | -| POST | * | HTTP Error 403 | -| PUT | * | HTTP Error 403 | -| PATCH | * | HTTP Error 403 | +| Method | Route | Target | +|--------|-------------------|---------------------------------------------| +| GET | /api/* | http://jsonplaceholder.typicode.com/* | +| GET | /api/customers/* | http://jsonplaceholder.typicode.com/users/* | +| POST | * | HTTP Error 403 | +| PUT | * | HTTP Error 403 | +| PATCH | * | HTTP Error 403 | diff --git a/examples/routing/config.ru b/examples/routing/config.ru index 7a8d146..58b97f9 100644 --- a/examples/routing/config.ru +++ b/examples/routing/config.ru @@ -1,4 +1,5 @@ require 'api_valve' +require 'byebug' app = Rack::Builder.new do use ApiValve::Middleware::ErrorHandling @@ -6,12 +7,15 @@ app = Rack::Builder.new do map '/api' do run ApiValve::Proxy.from_hash( - endpoint: 'http://api.host/api/', + endpoint: 'http://jsonplaceholder.typicode.com', routes: [ { method: 'get', - path: %r{^/prefix/(?.*)}, - request: {path: '%{final_path}'} + path: %r{^/customers/(?.*)}, + request: {path: '/users/%{path}'} + }, + { + method: 'get' }, { method: 'post', diff --git a/lib/api_valve.rb b/lib/api_valve.rb index 31c4636..c399868 100644 --- a/lib/api_valve.rb +++ b/lib/api_valve.rb @@ -12,6 +12,7 @@ require 'faraday' require 'multi_json' require 'logger' +require 'rack' module ApiValve autoload :Benchmarking, 'api_valve/benchmarking' diff --git a/lib/api_valve/error_responder.rb b/lib/api_valve/error_responder.rb index f81cb7e..4fff795 100644 --- a/lib/api_valve/error_responder.rb +++ b/lib/api_valve/error_responder.rb @@ -5,10 +5,10 @@ def initialize(error) end def call - [ + Rack::Response[ status, {'Content-Type' => 'application/vnd.api+json'}, - [MultiJson.dump({errors: [json_error]}, mode: :compat)] + MultiJson.dump({errors: [json_error]}, mode: :compat) ] end diff --git a/lib/api_valve/forwarder/response.rb b/lib/api_valve/forwarder/response.rb index 1845ed1..1956871 100644 --- a/lib/api_valve/forwarder/response.rb +++ b/lib/api_valve/forwarder/response.rb @@ -26,7 +26,7 @@ def initialize(original_request, original_response, options = {}) # Must return a rack compatible response array of status code, headers and body def rack_response - [status, headers, [body]] + Rack::Response.new(body, status, headers) end protected diff --git a/lib/api_valve/middleware/error_handling.rb b/lib/api_valve/middleware/error_handling.rb index 2d8d97c..ec0f25b 100644 --- a/lib/api_valve/middleware/error_handling.rb +++ b/lib/api_valve/middleware/error_handling.rb @@ -8,7 +8,7 @@ def call(env) @app.call(env) rescue Exception => e # rubocop:disable Lint/RescueException log_error e - self.class.const_get(ApiValve.error_responder).new(e).call + render_error(e).to_a end private @@ -17,5 +17,9 @@ def log_error(error) ApiValve.logger.error { "#{error.class}: #{error.message}" } ApiValve.logger.error { error.backtrace.join("\n") } end + + def render_error(error) + self.class.const_get(ApiValve.error_responder).new(error).call + end end end diff --git a/lib/api_valve/proxy.rb b/lib/api_valve/proxy.rb index d0a62e4..5e48270 100644 --- a/lib/api_valve/proxy.rb +++ b/lib/api_valve/proxy.rb @@ -24,9 +24,9 @@ def initialize(forwarder) end def call(env) - to_app.call(env) + to_app.call(env).to_a rescue ApiValve::Error::Client, ApiValve::Error::Server => e - render_error e + render_error(e).to_a end delegate :add_route, to: :route_set diff --git a/spec/api_valve/forwarder/response_spec.rb b/spec/api_valve/forwarder/response_spec.rb index 346a3b4..0d7e966 100644 --- a/spec/api_valve/forwarder/response_spec.rb +++ b/spec/api_valve/forwarder/response_spec.rb @@ -20,7 +20,7 @@ {'Location' => '/remote-prefix/see/other/path'} end let(:rack_response) { response.rack_response } - let(:headers) { rack_response[1] } + let(:headers) { rack_response.headers } describe 'Location header' do subject { headers['Location'] } diff --git a/spec/examples/middlewares_spec.rb b/spec/examples/middlewares_spec.rb index ceb0493..f76ddde 100644 --- a/spec/examples/middlewares_spec.rb +++ b/spec/examples/middlewares_spec.rb @@ -1,6 +1,5 @@ RSpec.describe 'Middleware example', type: :feature do - let(:builder) { example_app 'middleware' } - let(:app) { builder[0] } + let(:app) { example_app 'middleware' } before do stub_request(:get, %r{^http://api.host/api}) diff --git a/spec/examples/permissions_spec.rb b/spec/examples/permissions_spec.rb index 36f7d19..24e74c8 100644 --- a/spec/examples/permissions_spec.rb +++ b/spec/examples/permissions_spec.rb @@ -1,6 +1,5 @@ RSpec.describe 'Permissions example', type: :request do - let(:builder) { example_app 'permissions' } - let(:app) { builder[0] } + let(:app) { example_app 'permissions' } before do stub_request(:get, %r{^http://api.host/api}) diff --git a/spec/examples/rewriting_spec.rb b/spec/examples/rewriting_spec.rb index 9d47e3b..878f79d 100644 --- a/spec/examples/rewriting_spec.rb +++ b/spec/examples/rewriting_spec.rb @@ -1,6 +1,5 @@ RSpec.describe 'Rewriting example', type: :feature do - let(:builder) { example_app 'rewriting' } - let(:app) { builder[0] } + let(:app) { example_app 'rewriting' } before do stub_request(:get, %r{^http://api.host/api}) diff --git a/spec/examples/routing_spec.rb b/spec/examples/routing_spec.rb new file mode 100644 index 0000000..905204c --- /dev/null +++ b/spec/examples/routing_spec.rb @@ -0,0 +1,22 @@ +RSpec.describe 'Routing example', type: :feature do + let(:app) { example_app 'routing' } + + before do + stub_request(:get, %r{^http://jsonplaceholder.typicode.com/users}) + .to_return(status: 204, headers: {'Content-Type' => 'application/json'}) + end + + describe "GET '/api/customers/1'" do + it 'correctly forwards the request' do + get '/api/customers/1' + expect(WebMock).to(have_requested(:get, 'http://jsonplaceholder.typicode.com/users/1')) + end + end + + describe "GET '/api/users/2'" do + it 'correctly forwards the request' do + get '/api/users/2' + expect(WebMock).to(have_requested(:get, 'http://jsonplaceholder.typicode.com/users/2')) + end + end +end diff --git a/spec/support/helper.rb b/spec/support/helper.rb index bd498d2..04ad879 100644 --- a/spec/support/helper.rb +++ b/spec/support/helper.rb @@ -1,6 +1,8 @@ module Helper def example_app(example) path = Pathname.new(__FILE__).join("../../../examples/#{example}/config.ru") - Rack::Builder.parse_file(path.to_s) + # In rack 2.x parse_file returns a tuple + app, _config = *Rack::Builder.parse_file(path.to_s) + app end end