rack-berater 0.0.2 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a5812fba3c2523be30274bcc192d67ba7cebbb1d2b52dcc0ca403d69c39630c
4
- data.tar.gz: 46152472d4af26a86e506a9150ff9c56743490707b630ecddc94beb0e63f7448
3
+ metadata.gz: 3475519672f1074d749de982b36d3a2d0d15a468d38d35b91e0f237de02ba2f8
4
+ data.tar.gz: fd08c05ccc694adc8ca540a2468956144c0d180611dd4622070e15aa8dd120ef
5
5
  SHA512:
6
- metadata.gz: '079407a6fd8d63c849809c972ef15739de09bd490c1e3a4046ae047dbba8b36cb7e5f0606c17b5e5a6a74ce8ec9fa95f61139afc3937a5c30643fe85ff59e91d'
7
- data.tar.gz: 7d6ede65719ed13181c25aca35a48c941ee9c4777caa4f56eede0552b4acdc39d2989069659217327a0b3bc494d0c178b1ee1faa21e42f5c70dd4067f4fb077e
6
+ metadata.gz: 88428422463a296369f8a2f141c74dadc90f132e2d2072c3505bf33012e344668277e23146125cf43bca55a7d6ec83da0871d6e5b23684a72f3875e48f5d43d7
7
+ data.tar.gz: be42f6faa19469ccbfed7995f02b81533051b828e688779d8b5986be6a83174baf71c8ca399b9fabb419766a59fbd8a0c366eefdee977865d7c215970e815527
@@ -1,7 +1,7 @@
1
1
  require 'rails/railtie'
2
2
 
3
3
  module Rack
4
- module Berater
4
+ class Berater
5
5
  class Railtie < Rails::Railtie
6
6
  initializer "rack.berater.initializer" do |app|
7
7
  app.middleware.use Rack::Berater
@@ -1,5 +1,5 @@
1
1
  module Rack
2
- module Berater
3
- VERSION = "0.0.2"
2
+ class Berater
3
+ VERSION = "0.1.0"
4
4
  end
5
5
  end
data/lib/rack/berater.rb CHANGED
@@ -1,9 +1,61 @@
1
+ require "berater"
2
+ require "rack"
1
3
  require "rack/berater/version"
4
+ require "set"
2
5
 
3
6
  module Rack
4
- module Berater
5
- autoload :Handler, "rack/berater/handler"
6
- # autoload :Limiter, "rack/berater/limiter"
7
+ class Berater
7
8
  autoload :Railtie, "rack/berater/railtie"
9
+
10
+ ERROR_TYPES = Set.new([ ::Berater::Overloaded ])
11
+
12
+ def initialize(app, options = {})
13
+ @app = app
14
+ @enabled = options[:enabled?]
15
+ @limiter = options[:limiter]
16
+ @options = {
17
+ headers: {},
18
+ status_code: options.fetch(:status_code, 429),
19
+ }
20
+
21
+ # configure body
22
+ @options[:body] = case options[:body]
23
+ when true, nil
24
+ Rack::Utils::HTTP_STATUS_CODES[@options[:status_code]]
25
+ when false
26
+ nil
27
+ when String
28
+ options[:body]
29
+ else
30
+ raise ArgumentError, "invalid :body option: #{options[:body]}"
31
+ end
32
+
33
+ # configure headers
34
+ if @options[:body]
35
+ @options[:headers][Rack::CONTENT_TYPE] = "text/plain"
36
+ end
37
+ @options[:headers].update(options.fetch(:headers, {}))
38
+ end
39
+
40
+ def call(env)
41
+ if enabled?(env)
42
+ @limiter.limit { @app.call(env) }
43
+ else
44
+ @app.call(env)
45
+ end
46
+ rescue *ERROR_TYPES => e
47
+ [
48
+ @options[:status_code],
49
+ @options[:headers],
50
+ [ @options[:body] ].compact,
51
+ ]
52
+ end
53
+
54
+ private
55
+
56
+ def enabled?(env)
57
+ return false unless @limiter
58
+ @enabled.nil? ? true : @enabled.call(env)
59
+ end
8
60
  end
9
61
  end
data/lib/rack-berater.rb CHANGED
@@ -1,3 +1 @@
1
- require "berater"
2
- require "rack"
3
1
  require "rack/berater"
@@ -0,0 +1,96 @@
1
+ describe Rack::Berater do
2
+ before do
3
+ app.use described_class, limiter: limiter, enabled?: enabled?
4
+ end
5
+ let(:limiter) { nil }
6
+ let(:enabled?) { nil }
7
+
8
+ let(:app) do
9
+ Rack::Builder.new do
10
+ use Rack::Lint
11
+ run (lambda do |env|
12
+ [200, {"Content-Type" => "text/plain"}, ["OK"]]
13
+ end)
14
+ end
15
+ end
16
+ let(:response) { get "/" }
17
+
18
+ shared_examples "works nominally" do
19
+ it "has the correct status code" do
20
+ expect(response.status).to eq 200
21
+ end
22
+
23
+ it "has the correct headers" do
24
+ expect(response.headers).to eq({
25
+ "Content-Type" => "text/plain",
26
+ "Content-Length" => "2",
27
+ })
28
+ end
29
+
30
+ it "has the correct body" do
31
+ expect(response.body).to eq "OK"
32
+ end
33
+ end
34
+
35
+ context "without a limiter" do
36
+ before { Berater.test_mode = :fail }
37
+
38
+ include_examples "works nominally"
39
+ end
40
+
41
+ describe "limiter option" do
42
+ let(:limiter) { ::Berater::Unlimiter.new }
43
+
44
+ include_examples "works nominally"
45
+
46
+ it "calls the limiter" do
47
+ expect(limiter).to receive(:limit).and_call_original
48
+ response
49
+ end
50
+
51
+ context "when operating beyond limits" do
52
+ before { Berater.test_mode = :fail }
53
+
54
+ it "returns an error" do
55
+ expect(response.status).to eq 429
56
+ end
57
+ end
58
+ end
59
+
60
+ describe "enabled? option" do
61
+ after { expect(response.status).to eq 200 }
62
+
63
+ let(:enabled?) { double }
64
+
65
+ context "when there is a limiter" do
66
+ let(:limiter) { ::Berater::Unlimiter.new }
67
+
68
+ it "should be called with the env hash" do
69
+ expect(enabled?).to receive(:call) do |env|
70
+ expect(env).to be_a Hash
71
+ expect(Rack::Request.new(env).path).to eq "/"
72
+ end
73
+ end
74
+
75
+ context "when enabled" do
76
+ it "should call the limiter" do
77
+ expect(enabled?).to receive(:call).and_return(true)
78
+ expect(limiter).to receive(:limit).and_call_original
79
+ end
80
+ end
81
+
82
+ context "when disabled" do
83
+ it "should not call the limiter" do
84
+ expect(enabled?).to receive(:call).and_return(false)
85
+ expect(limiter).not_to receive(:limit)
86
+ end
87
+ end
88
+ end
89
+
90
+ context "when there is no limiter" do
91
+ it "should not call enabled?" do
92
+ expect(enabled?).not_to receive(:call)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,145 @@
1
+ describe Rack::Berater do
2
+ let(:app) do
3
+ Rack::Builder.new do
4
+ use Rack::Lint
5
+ run (lambda do |env|
6
+ Berater::Unlimiter() do
7
+ [200, {"Content-Type" => "text/plain"}, ["OK"]]
8
+ end
9
+ end)
10
+ end
11
+ end
12
+ let(:response) { get "/" }
13
+
14
+ shared_examples "works nominally" do
15
+ it "has the correct status code" do
16
+ expect(response.status).to eq 200
17
+ end
18
+
19
+ it "has the correct headers" do
20
+ expect(response.headers).to eq({
21
+ "Content-Type" => "text/plain",
22
+ "Content-Length" => "2",
23
+ })
24
+ end
25
+
26
+ it "has the correct body" do
27
+ expect(response.body).to eq "OK"
28
+ end
29
+ end
30
+
31
+ context "without middleware" do
32
+ include_examples "works nominally"
33
+
34
+ it "does not catch limit errors" do
35
+ Berater.test_mode = :fail
36
+ expect {
37
+ response
38
+ }.to be_overloaded
39
+ end
40
+ end
41
+
42
+ context "with middleware using default settings" do
43
+ before { app.use described_class }
44
+
45
+ include_examples "works nominally"
46
+
47
+ it "catches and transforms limit errors" do
48
+ Berater.test_mode = :fail
49
+ expect(response.status).to eq 429
50
+ expect(response.body).to eq "Too Many Requests"
51
+ end
52
+ end
53
+
54
+ context "with middleware using custom settings" do
55
+ before do
56
+ app.use described_class, options
57
+ Berater.test_mode = :fail
58
+ end
59
+
60
+ context "with a custom body" do
61
+ context "with body nil" do
62
+ let(:options) { { body: nil } }
63
+
64
+ it "should default to sending a body" do
65
+ expect(response.body).to eq "Too Many Requests"
66
+ end
67
+ end
68
+
69
+ context "with body disabled" do
70
+ let(:options) { { body: false } }
71
+
72
+ it "should not send a body" do
73
+ expect(response.body).to be_empty
74
+ end
75
+
76
+ it "should not send the Content-Type header" do
77
+ expect(response.headers.keys).not_to include(Rack::CONTENT_TYPE)
78
+ end
79
+ end
80
+
81
+ context "with a string" do
82
+ let(:body) { "none shall pass!" }
83
+ let(:options) { { body: body } }
84
+
85
+ it "should send the custom string" do
86
+ expect(response.body).to eq body
87
+ end
88
+ end
89
+ end
90
+
91
+ context "with custom headers" do
92
+ context "with an extra header" do
93
+ let(:options) { { headers: { Rack::CACHE_CONTROL => "no-cache" } } }
94
+
95
+ it "should contain the default headers" do
96
+ expect(response.headers.keys).to include(Rack::CONTENT_TYPE)
97
+ end
98
+
99
+ it "should also contain the custom header" do
100
+ expect(response.headers).to include(options[:headers])
101
+ end
102
+ end
103
+
104
+ context "with a new content type" do
105
+ let(:options) { { headers: { Rack::CONTENT_TYPE => "application/json" } } }
106
+
107
+ it "should override the Content-Type header" do
108
+ expect(response.headers).to include(options[:headers])
109
+ end
110
+ end
111
+ end
112
+
113
+ context "with custom status code" do
114
+ let(:options) { { status_code: 503 } }
115
+
116
+ it "catches and transforms limit errors" do
117
+ expect(response.status).to eq 503
118
+ expect(response.body).to eq "Service Unavailable"
119
+ end
120
+ end
121
+ end
122
+
123
+ context "with custom error type" do
124
+ before do
125
+ app.use described_class
126
+ expect(Berater::Limiter).to receive(:new).and_raise(IOError)
127
+ end
128
+
129
+ it "normally crashes the app" do
130
+ expect { response }.to raise_error(IOError)
131
+ end
132
+
133
+ context "when an error type is registered with middleware" do
134
+ around do |example|
135
+ Rack::Berater::ERROR_TYPES << IOError
136
+ example.run
137
+ Rack::Berater::ERROR_TYPES.delete(IOError)
138
+ end
139
+
140
+ it "catches and transforms limit errors" do
141
+ expect(response.status).to eq 429
142
+ end
143
+ end
144
+ end
145
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-berater
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Pepper
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-13 00:00:00.000000000 Z
11
+ date: 2021-10-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: berater
@@ -116,10 +116,10 @@ extra_rdoc_files: []
116
116
  files:
117
117
  - lib/rack-berater.rb
118
118
  - lib/rack/berater.rb
119
- - lib/rack/berater/handler.rb
120
119
  - lib/rack/berater/railtie.rb
121
120
  - lib/rack/berater/version.rb
122
- - spec/handler_spec.rb
121
+ - spec/limiter_spec.rb
122
+ - spec/rescuer_spec.rb
123
123
  homepage: https://github.com/dpep/rack-berater
124
124
  licenses:
125
125
  - MIT
@@ -139,9 +139,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
139
  - !ruby/object:Gem::Version
140
140
  version: '0'
141
141
  requirements: []
142
- rubygems_version: 3.0.8
142
+ rubygems_version: 3.1.4
143
143
  signing_key:
144
144
  specification_version: 4
145
145
  summary: Rack::Berater
146
146
  test_files:
147
- - spec/handler_spec.rb
147
+ - spec/rescuer_spec.rb
148
+ - spec/limiter_spec.rb
@@ -1,43 +0,0 @@
1
- module Rack
2
- module Berater
3
- class Handler
4
- def initialize(app, options = {})
5
- @app = app
6
- @options = {
7
- status_code: options.fetch(:status_code, 429),
8
- headers: {
9
- Rack::CONTENT_TYPE => "text/plain",
10
- }.update(options.fetch(:headers, {})),
11
- body: options.fetch(:body, true),
12
- }
13
- end
14
-
15
- def call(env)
16
- @app.call(env)
17
- rescue ::Berater::Overloaded => e
18
- code = @options[:status_code]
19
-
20
- body = case @options[:body]
21
- when true
22
- Rack::Utils::HTTP_STATUS_CODES[code]
23
- when nil, false
24
- nil
25
- when String
26
- @options[:body]
27
- when Proc
28
- @options[:body].call(env, e)
29
- else
30
- raise ArgumentError, "invalid :body option: #{@options[:body]}"
31
- end
32
-
33
- headers = body ? @options[:headers] : {}
34
-
35
- [
36
- code,
37
- headers,
38
- [ body ].compact,
39
- ]
40
- end
41
- end
42
- end
43
- end
data/spec/handler_spec.rb DELETED
@@ -1,126 +0,0 @@
1
- describe Rack::Berater::Handler do
2
- let(:app) do
3
- Rack::Builder.new do
4
- use Rack::Lint
5
- run (lambda do |env|
6
- raise ::Berater::Overloaded if ::Berater.test_mode == :fail
7
- [200, {"Content-Type" => "text/plain"}, ["OK"]]
8
- end)
9
- end
10
- end
11
- let(:response) { get "/" }
12
-
13
- shared_examples "works nominally" do
14
- it "has the correct status code" do
15
- expect(response.status).to eq 200
16
- end
17
-
18
- it "has the correct headers" do
19
- expect(response.headers).to eq({
20
- "Content-Type" => "text/plain",
21
- "Content-Length" => "2",
22
- })
23
- end
24
-
25
- it "has the correct body" do
26
- expect(response.body).to eq "OK"
27
- end
28
- end
29
-
30
- context "without Handler" do
31
- include_examples "works nominally"
32
-
33
- it "does not catch limit errors" do
34
- Berater.test_mode = :fail
35
- expect {
36
- response
37
- }.to be_overloaded
38
- end
39
- end
40
-
41
- context "with Handler using default settings" do
42
- context "with default settings" do
43
- before { app.use described_class }
44
-
45
- include_examples "works nominally"
46
-
47
- it "catches and transforms limit errors" do
48
- Berater.test_mode = :fail
49
- expect(response.status).to eq 429
50
- expect(response.body).to eq "Too Many Requests"
51
- end
52
- end
53
- end
54
-
55
- context "with Handler using custom settings" do
56
- before do
57
- app.use described_class, options
58
- Berater.test_mode = :fail
59
- end
60
-
61
- context "with custom status code" do
62
- let(:options) { { status_code: 503 } }
63
-
64
- it "catches and transforms limit errors" do
65
- expect(response.status).to eq 503
66
- expect(response.body).to eq "Service Unavailable"
67
- end
68
- end
69
-
70
- context "with body disabled" do
71
- let(:options) { { body: false } }
72
-
73
- it "should not send a body" do
74
- expect(response.body).to be_empty
75
- end
76
-
77
- it "should not send the Content-Type header" do
78
- expect(response.headers.keys).not_to include(Rack::CONTENT_TYPE)
79
- end
80
- end
81
-
82
- context "with body nil" do
83
- let(:options) { { body: nil } }
84
-
85
- it "should not send a body" do
86
- expect(response.body).to be_empty
87
- end
88
- end
89
-
90
- context "with custom body" do
91
- let(:body) { "none shall pass!" }
92
- let(:options) { { body: body } }
93
-
94
- it "should send the custom string" do
95
- expect(response.body).to eq body
96
- end
97
- end
98
-
99
- context "with a dynamic body" do
100
- let(:body) { "none shall pass!" }
101
- let(:fn) { proc { body } }
102
- let(:options) { { body: fn } }
103
-
104
- it "should call the Proc and send the result" do
105
- expect(response.body).to eq body
106
- end
107
-
108
- it "should pass in the env and error" do
109
- expect(fn).to receive(:call).with(Hash, ::Berater::Overloaded)
110
- response
111
- end
112
- end
113
-
114
- context "with custom headers" do
115
- let(:options) { { headers: { Rack::CACHE_CONTROL => "no-cache" } } }
116
-
117
- it "should contain the default headers" do
118
- expect(response.headers.keys).to include(Rack::CONTENT_TYPE)
119
- end
120
-
121
- it "should also contain custom header" do
122
- expect(response.headers).to include(options[:headers])
123
- end
124
- end
125
- end
126
- end