diff --git a/.rubocop.yml b/.rubocop.yml index 4131cc4..af032d6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -17,3 +17,9 @@ Style/FrozenStringLiteralComment: Enabled: false inherit_from: .rubocop_todo.yml + +require: + - rubocop-capybara + - rubocop-rake + - rubocop-rspec + diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index e6dbdcf..ffa2449 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,15 +1,110 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2022-06-18 16:28:15 UTC using RuboCop version 1.30.1. +# on 2023-11-08 18:43:53 UTC using RuboCop version 1.57.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 1 -Lint/UriEscapeUnescape: +# This cop supports unsafe autocorrection (--autocorrect-all). +RSpec/BeEql: Exclude: - - 'api/upload_file.rb' + - 'spec/api/documentation_spec.rb' + +# Offense count: 14 +# Configuration parameters: Prefixes, AllowedPatterns. +# Prefixes: when, with, without +RSpec/ContextWording: + Exclude: + - 'spec/api/content_type_spec.rb' + - 'spec/api/cors_spec.rb' + - 'spec/api/documentation_spec.rb' + - 'spec/api/header_versioning_spec.rb' + - 'spec/api/post_put_spec.rb' + - 'spec/integration/grape_on_rack_spec.rb' + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +RSpec/EmptyExampleGroup: + Exclude: + - 'spec/api/documentation_spec.rb' + +# Offense count: 12 +# Configuration parameters: CountAsOne. +RSpec/ExampleLength: + Max: 11 + +# Offense count: 3 +RSpec/ExpectInHook: + Exclude: + - 'spec/api/documentation_spec.rb' + +# Offense count: 15 +# Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly. +# Include: **/*_spec*rb*, **/spec/**/* +RSpec/FilePath: + Exclude: + - 'spec/api/content_type_spec.rb' + - 'spec/api/cors_spec.rb' + - 'spec/api/documentation_spec.rb' + - 'spec/api/entities_spec.rb' + - 'spec/api/get_json_spec.rb' + - 'spec/api/header_versioning_spec.rb' + - 'spec/api/headers_spec.rb' + - 'spec/api/path_versioning_spec.rb' + - 'spec/api/ping_spec.rb' + - 'spec/api/post_json_spec.rb' + - 'spec/api/post_put_spec.rb' + - 'spec/api/rescue_from_spec.rb' + - 'spec/api/stream_data_spec.rb' + - 'spec/api/upload_file_spec.rb' + - 'spec/api/wrap_response_spec.rb' + +# Offense count: 9 +# Configuration parameters: AssignmentOnly. +RSpec/InstanceVariable: + Exclude: + - 'spec/api/documentation_spec.rb' + - 'spec/api/post_put_spec.rb' + - 'spec/integration/grape_on_rack_spec.rb' + +# Offense count: 26 +RSpec/MultipleExpectations: + Max: 4 + +# Offense count: 2 +RSpec/RepeatedExample: + Exclude: + - 'spec/api/headers_spec.rb' + +# Offense count: 15 +# Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata. +# Include: **/*_spec.rb +RSpec/SpecFilePathFormat: + Exclude: + - '**/spec/routing/**/*' + - 'spec/api/content_type_spec.rb' + - 'spec/api/cors_spec.rb' + - 'spec/api/documentation_spec.rb' + - 'spec/api/entities_spec.rb' + - 'spec/api/get_json_spec.rb' + - 'spec/api/header_versioning_spec.rb' + - 'spec/api/headers_spec.rb' + - 'spec/api/path_versioning_spec.rb' + - 'spec/api/ping_spec.rb' + - 'spec/api/post_json_spec.rb' + - 'spec/api/post_put_spec.rb' + - 'spec/api/rescue_from_spec.rb' + - 'spec/api/stream_data_spec.rb' + - 'spec/api/upload_file_spec.rb' + - 'spec/api/wrap_response_spec.rb' + +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +Rake/Desc: + Exclude: + - 'Rakefile' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). @@ -35,3 +130,10 @@ Style/OpenStructUse: Style/RescueModifier: Exclude: - 'api/get_json.rb' + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Mode. +Style/StringConcatenation: + Exclude: + - 'api/stream_data.rb' diff --git a/Gemfile b/Gemfile index 0144f34..e64f49c 100644 --- a/Gemfile +++ b/Gemfile @@ -8,9 +8,10 @@ gem 'json' gem 'mime-types' gem 'newrelic_rpm' gem 'nokogiri' -gem 'rack', '< 3' +gem 'puma' +gem 'rack' gem 'rack-cors' -gem 'webrick' +gem 'rackup' group :development do gem 'guard' @@ -18,6 +19,7 @@ group :development do gem 'guard-rack' gem 'rake' gem 'rubocop' + gem 'rubocop-capybara' gem 'rubocop-rake' gem 'rubocop-rspec' end diff --git a/Gemfile.lock b/Gemfile.lock index ea9aba9..086db23 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,16 +1,23 @@ GEM remote: http://rubygems.org/ specs: - activesupport (7.0.7.2) + activesupport (7.1.1) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) - addressable (2.8.1) + addressable (2.8.5) public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) + base64 (0.2.0) + bigdecimal (3.1.4) builder (3.2.4) - capybara (3.38.0) + capybara (3.39.2) addressable matrix mini_mime (>= 0.1.3) @@ -21,7 +28,10 @@ GEM xpath (~> 3.2) coderay (1.1.3) concurrent-ruby (1.2.2) + connection_pool (2.4.1) diff-lcs (1.5.0) + drb (2.2.0) + ruby2_keywords dry-core (1.0.0) concurrent-ruby (~> 1.0) zeitwerk (~> 2.6) @@ -36,10 +46,10 @@ GEM dry-inflector (~> 1.0) dry-logic (~> 1.4) zeitwerk (~> 2.6) - ffi (1.15.5) + ffi (1.16.3) formatador (1.1.0) - grape (1.7.0) - activesupport + grape (1.8.0) + activesupport (>= 5) builder dry-types (>= 1.1) mustermann-grape (~> 1.0.0) @@ -48,12 +58,12 @@ GEM grape-entity (1.0.0) activesupport (>= 3.0.0) multi_json (>= 1.3.2) - grape-swagger (1.6.0) + grape-swagger (1.6.1) grape (~> 1.3) - grape-swagger-entity (0.5.1) + grape-swagger-entity (0.5.2) grape-entity (>= 0.6.0) grape-swagger (>= 1.2.0) - guard (2.18.0) + guard (2.18.1) formatador (>= 0.2.4) listen (>= 2.7, < 4.0) lumberjack (>= 1.0.12, < 2.0) @@ -74,104 +84,118 @@ GEM i18n (1.14.1) concurrent-ruby (~> 1.0) json (2.6.3) + language_server-protocol (3.17.0.3) listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - lumberjack (1.2.8) + lumberjack (1.2.9) matrix (0.4.2) method_source (1.0.0) - mime-types (3.4.1) + mime-types (3.5.1) mime-types-data (~> 3.2015) - mime-types-data (3.2023.0218.1) - mini_mime (1.1.2) - mini_portile2 (2.8.1) - minitest (5.19.0) + mime-types-data (3.2023.1003) + mini_mime (1.1.5) + mini_portile2 (2.8.5) + minitest (5.20.0) multi_json (1.15.0) mustermann (3.0.0) ruby2_keywords (~> 0.0.1) mustermann-grape (1.0.2) mustermann (>= 1.0.0) + mutex_m (0.2.0) nenv (0.3.0) - newrelic_rpm (9.0.0) - nokogiri (1.14.3) - mini_portile2 (~> 2.8.0) + newrelic_rpm (9.6.0) + base64 + nio4r (2.5.9) + nokogiri (1.15.4) + mini_portile2 (~> 2.8.2) racc (~> 1.4) notiffany (0.1.3) nenv (~> 0.1) shellany (~> 0.0) - parallel (1.22.1) - parser (3.2.1.1) + parallel (1.23.0) + parser (3.2.2.4) ast (~> 2.4.1) + racc pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) - public_suffix (5.0.1) - racc (1.6.2) - rack (2.2.6.4) + public_suffix (5.0.3) + puma (6.4.0) + nio4r (~> 2.0) + racc (1.7.3) + rack (3.0.8) rack-accept (0.4.5) rack (>= 0.4) rack-cors (2.0.1) rack (>= 2.0.0) rack-test (2.1.0) rack (>= 1.3) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) rainbow (3.1.1) - rake (13.0.6) + rake (13.1.0) rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - regexp_parser (2.7.0) - rexml (3.2.5) + regexp_parser (2.8.2) + rexml (3.2.6) rspec (3.12.0) rspec-core (~> 3.12.0) rspec-expectations (~> 3.12.0) rspec-mocks (~> 3.12.0) - rspec-core (3.12.1) + rspec-core (3.12.2) rspec-support (~> 3.12.0) - rspec-expectations (3.12.2) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-mocks (3.12.4) + rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-support (3.12.0) - rubocop (1.48.1) + rspec-support (3.12.1) + rubocop (1.57.2) json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.0.0) + parser (>= 3.2.2.4) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.26.0, < 2.0) + rubocop-ast (>= 1.28.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.28.0) + rubocop-ast (1.30.0) parser (>= 3.2.1.0) - rubocop-capybara (2.17.1) + rubocop-capybara (2.19.0) rubocop (~> 1.41) + rubocop-factory_bot (2.24.0) + rubocop (~> 1.33) rubocop-rake (0.6.0) rubocop (~> 1.0) - rubocop-rspec (2.19.0) - rubocop (~> 1.33) + rubocop-rspec (2.25.0) + rubocop (~> 1.40) rubocop-capybara (~> 2.17) + rubocop-factory_bot (~> 2.22) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) rubyzip (2.3.2) - selenium-webdriver (4.8.2) + selenium-webdriver (4.9.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) shellany (0.0.1) spoon (0.0.6) ffi - thor (1.2.1) + thor (1.3.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.4.2) + unicode-display_width (2.5.0) webrick (1.8.1) - websocket (1.2.9) + websocket (1.2.10) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.7) + zeitwerk (2.6.12) PLATFORMS ruby @@ -189,16 +213,18 @@ DEPENDENCIES mime-types newrelic_rpm nokogiri - rack (< 3) + puma + rack rack-cors rack-test + rackup rake rspec rubocop + rubocop-capybara rubocop-rake rubocop-rspec selenium-webdriver - webrick BUNDLED WITH - 2.3.15 + 2.4.2 diff --git a/README.md b/README.md index b451e43..ba7cbbf 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,14 @@ $ bundle install $ rackup Loading NewRelic in developer mode ... -[2013-06-20 08:57:58] INFO WEBrick 1.3.1 -[2013-06-20 08:57:58] INFO ruby 1.9.3 (2013-02-06) [x86_64-darwin11.4.2] -[2013-06-20 08:57:58] INFO WEBrick::HTTPServer#start: pid=247 port=9292 +Puma starting in single mode... +* Puma version: 6.4.0 (ruby 2.7.7-p221) ("The Eagle of Durango") +* Min threads: 0 +* Max threads: 5 +* Environment: development +* PID: 82944 +* Listening on http://127.0.0.1:9292 +* Listening on http://[::1]:9292 ``` List Routes @@ -229,6 +234,18 @@ $ curl http://localhost:9292/api/headers/Host {"Host":"localhost:9292"} ``` +### [stream_data](api/stream_data.rb) + +An example of streaming data. + +``` +curl http://localhost:9292/api/stream --no-buffer +1 +2 +3 +... +``` + New Relic --------- diff --git a/api/stream_data.rb b/api/stream_data.rb new file mode 100644 index 0000000..6b739b0 --- /dev/null +++ b/api/stream_data.rb @@ -0,0 +1,19 @@ +module Acme + class SlowStreamer + def each + (1..5).each do |i| + yield (i.to_s + "\n") + sleep 0.3 + end + end + end + + class StreamData < Grape::API + desc 'Streams data.' + get :stream do + stream SlowStreamer.new + content_type 'text/event-stream' + status 200 + end + end +end diff --git a/app/api.rb b/app/api.rb index 3378cd2..1658f6a 100644 --- a/app/api.rb +++ b/app/api.rb @@ -14,6 +14,7 @@ class API < Grape::API mount ::Acme::UploadFile mount ::Acme::Entities::API mount ::Acme::Headers + mount ::Acme::StreamData add_swagger_documentation api_version: 'v1' end end diff --git a/public/index.html b/public/index.html index cbc0ce0..4873bcf 100644 --- a/public/index.html +++ b/public/index.html @@ -3,6 +3,7 @@ Rack Powers Web APIs +

Rack Powers Web APIs

@@ -14,9 +15,10 @@

Rack Powers Web APIs

  • fetching ring value ... - +
  • +
  • fetching stream data ...
  • diff --git a/public/scripts/stream.js b/public/scripts/stream.js new file mode 100644 index 0000000..0d62b6a --- /dev/null +++ b/public/scripts/stream.js @@ -0,0 +1,13 @@ +$(document).ready(function() { + + var stream = function() { + var client = new XMLHttpRequest(); + client.open('get', '/api/stream'); + client.send(); + client.onprogress = function() { + $('#stream_value').html($('#stream_value').html() + "
    " + this.responseText); + } + }; + + stream(); +}); diff --git a/spec/api/cors_spec.rb b/spec/api/cors_spec.rb index fb8ad2b..f543346 100644 --- a/spec/api/cors_spec.rb +++ b/spec/api/cors_spec.rb @@ -18,11 +18,13 @@ def app expect(last_response.headers['Access-Control-Allow-Origin']).to eq('*') expect(last_response.headers['Access-Control-Expose-Headers']).to eq('') end + it 'includes Access-Control-Allow-Origin in the response' do get '/api/ping', {}, 'HTTP_ORIGIN' => 'http://cors.example.com' expect(last_response.status).to eq(200) expect(last_response.headers['Access-Control-Allow-Origin']).to eq('*') end + it 'includes Access-Control-Allow-Origin in errors' do get '/invalid', {}, 'HTTP_ORIGIN' => 'http://cors.example.com' expect(last_response.status).to eq(404) diff --git a/spec/api/header_versioning_spec.rb b/spec/api/header_versioning_spec.rb index ad89bae..de637b8 100644 --- a/spec/api/header_versioning_spec.rb +++ b/spec/api/header_versioning_spec.rb @@ -13,6 +13,7 @@ def app expect(last_response.status).to eq(200) expect(last_response.body).to eq({ header: 'acme' }.to_json) end + it 'invalid version' do get '/api', nil, 'HTTP_ACCEPT' => 'application/vnd.acme-v2+json' expect(last_response.status).to eq(404) diff --git a/spec/api/headers_spec.rb b/spec/api/headers_spec.rb index 83bc80c..6c6fd8f 100644 --- a/spec/api/headers_spec.rb +++ b/spec/api/headers_spec.rb @@ -11,8 +11,7 @@ def app get '/api/headers' expect(JSON.parse(last_response.body)).to eq( 'Cookie' => '', - 'Host' => 'example.org', - 'Version' => 'HTTP/1.0' + 'Host' => 'example.org' ) end @@ -39,8 +38,7 @@ def app expect(JSON.parse(last_response.body)).to eq( 'Cookie' => '', 'Host' => 'example.org', - 'Reticulated-Spline' => 42, - 'Version' => 'HTTP/1.0' + 'Reticulated-Spline' => 42 ) end end diff --git a/spec/api/post_put_spec.rb b/spec/api/post_put_spec.rb index 2711446..2062384 100644 --- a/spec/api/post_put_spec.rb +++ b/spec/api/post_put_spec.rb @@ -11,11 +11,13 @@ def app get '/api/ring' expect(JSON.parse(last_response.body)[:rang].to_i).to be >= 0 end + context 'with rings' do - before :each do + before do get '/api/ring' @rang = JSON.parse(last_response.body)['rang'].to_i end + it 'POST ring' do 2.times do |i| post '/api/ring' @@ -25,12 +27,14 @@ def app get '/api/ring' expect(last_response.body).to eq({ rang: @rang + 2 }.to_json) end + context 'PUT ring' do it 'succeeds for 2 rings' do put '/api/ring?count=2' expect(last_response.status).to eq(200) expect(last_response.body).to eq({ rang: @rang + 2 }.to_json) end + it 'fails with a missing ring' do put '/api/ring' expect(last_response.status).to eq(400) diff --git a/spec/api/stream_data_spec.rb b/spec/api/stream_data_spec.rb new file mode 100644 index 0000000..9c3883f --- /dev/null +++ b/spec/api/stream_data_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Acme::API do + include Rack::Test::Methods + + def app + Acme::API + end + + it 'returns all data' do + get '/api/stream' + expect(last_response.status).to eq(200) + expect(last_response.instance_variable_get(:@body)).to eq(%W[1\n 2\n 3\n 4\n 5\n]) + expect(last_response.body).to eq("1\n2\n3\n4\n5\n") + expect(last_response.content_type).to eq('text/event-stream') + end +end diff --git a/spec/integration/grape_on_rack_spec.rb b/spec/integration/grape_on_rack_spec.rb index 80a3c75..51d074f 100644 --- a/spec/integration/grape_on_rack_spec.rb +++ b/spec/integration/grape_on_rack_spec.rb @@ -1,48 +1,66 @@ require 'spec_helper' -describe 'Grape on RACK', js: true, type: :feature do +describe 'Grape on RACK', :js, type: :feature do context 'homepage' do it 'displays index.html page' do visit '/' expect(title).to eq('Rack Powers Web APIs') end + context 'ring' do - before :each do + before do @rang = Acme::PostPut.rang visit '/' end + it 'increments the ring counter' do - expect(find('#ring_value')).to have_content "rang #{@rang + 1} time(s)" - expect(find('#ring_action')).to have_content 'click here to ring again' + expect(find_by_id('ring_value')).to have_content "rang #{@rang + 1} time(s)" + expect(find_by_id('ring_action')).to have_content 'click here to ring again' 3.times do |i| - find('#ring_action').click - expect(find('#ring_value')).to have_content "rang #{@rang + i + 2} time(s)" + find_by_id('ring_action').click + expect(find_by_id('ring_value')).to have_content "rang #{@rang + i + 2} time(s)" end end end + + context 'stream' do + before do + visit '/' + end + + it 'fetches stream data incrementally' do + expect(find_by_id('stream_value')).to have_content "fetching stream data ...\n1\n1 2\n1 2 3\n1 2 3 4\n1 2 3 4 5" + end + end end + context "page that doesn't exist" do - before :each do + before do visit '/invalid' end + it 'displays 404 page' do expect(title).to eq('Page Not Found') end end + context 'Swagger UI' do it 'displays Swagger UI' do visit '/swagger/index.html' expect(title).to eq('Swagger UI') end end + context 'exception' do - before :each do + before do visit '/api/raise' end + it 'displays 500 page' do expect(title).to eq('Unexpected Error') end end + context 'curl' do it 'reticulates a spline' do visit '/' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3ca6bd5..29099b0 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -19,5 +19,5 @@ Capybara.configure do |config| config.app = Acme::App.new config.server_port = 9293 - config.server = :webrick + config.server = :puma end