From ad78267bcce7ce0b243248b4fbe939f1aaa7e430 Mon Sep 17 00:00:00 2001 From: Cyrille Courtiere Date: Thu, 21 Nov 2019 20:49:26 +0100 Subject: [PATCH 1/2] add ability to ignore selected middlewares --- lib/rack/freeze.rb | 19 ++++++++++++--- lib/rack/freeze/builder.rb | 33 ++++++++++++++++++------- lib/rack/freeze/configuration.rb | 31 ++++++++++++++++++++++++ spec/rack/freeze/builder.rb | 41 +++++++++++++++++++------------- spec/rack/freeze/builder_spec.rb | 23 +++++++++++++----- 5 files changed, 112 insertions(+), 35 deletions(-) create mode 100644 lib/rack/freeze/configuration.rb diff --git a/lib/rack/freeze.rb b/lib/rack/freeze.rb index d6ba9d6..d5000cd 100644 --- a/lib/rack/freeze.rb +++ b/lib/rack/freeze.rb @@ -1,15 +1,15 @@ # Copyright, 2017, by Samuel G. D. Williams. -# +# # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: -# +# # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. -# +# # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -19,7 +19,20 @@ # THE SOFTWARE. require_relative 'freeze/version' +require_relative 'freeze/configuration' require_relative 'freeze/builder' +module Rack + module Freeze + def self.configure + yield(configuration) + end + + def self.configuration + @configuration ||= Rack::Freeze::Configuration.new + end + end +end + # Enforce the policy globally. Rack::Builder.prepend(Rack::Freeze::Builder) diff --git a/lib/rack/freeze/builder.rb b/lib/rack/freeze/builder.rb index 806d703..8d9eb69 100644 --- a/lib/rack/freeze/builder.rb +++ b/lib/rack/freeze/builder.rb @@ -1,15 +1,15 @@ # Copyright, 2017, by Samuel G. D. Williams. -# +# # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: -# +# # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. -# +# # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -25,16 +25,31 @@ module Rack module Freeze module Builder def use(klass, *args, &block) - super Freezer.new(klass), *args, &block + if ignored_middleware?(klass) + super(klass, *args, &block) + else + super(Freezer.new(klass), *args, &block) + end end - - def run(app) - super app.freeze + + def run(app) + if ignored_middleware?(app.class) + super(app) + else + super(app.freeze) + end end - + def to_app - super.freeze + app = super + ignored_middleware?(app.class) ? app : app.freeze end + + private + + def ignored_middleware?(klass) + Rack::Freeze.configuration.ignored_middlewares.include?(klass) + end end end end diff --git a/lib/rack/freeze/configuration.rb b/lib/rack/freeze/configuration.rb new file mode 100644 index 0000000..003a931 --- /dev/null +++ b/lib/rack/freeze/configuration.rb @@ -0,0 +1,31 @@ +# Copyright, 2019, by Klaxit +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +module Rack + module Freeze + class Configuration + attr_accessor :ignored_middlewares + + def initialize + @ignored_middlewares = [] + end + end + end +end diff --git a/spec/rack/freeze/builder.rb b/spec/rack/freeze/builder.rb index 4a5336a..92c00dd 100644 --- a/spec/rack/freeze/builder.rb +++ b/spec/rack/freeze/builder.rb @@ -2,34 +2,41 @@ RSpec.shared_examples_for "frozen middleware" do it "should be entirely frozen" do current = builder.to_app - + expect(current).to_not be_nil - + # This can't traverse into Rack::URLMap and I'm not really sure how it should do that. while current expect(current).to be_frozen - + current = current.respond_to?(:app) ? current.app : nil end end + + it "should not be frozen if configuration ignores it" do + allow(Rack::Freeze.configuration).to receive(:ignored_middlewares) + .and_return([described_class]) + + expect(builder.to_app).to_not be_frozen + end end RSpec.shared_context "middleware builder" do let(:env) {Hash.new} - + let(:builder) do Rack::Builder.new.tap do |builder| builder.use described_class builder.run lambda { |env| [404, {}, []] } end end - + let(:app) {builder.to_app} - + it "should generate a valid app" do expect(app).to_not be_nil end - + it_behaves_like "frozen middleware" end @@ -37,13 +44,13 @@ class FaultyMiddleware def initialize(app) @app = app end - + attr :app - + # Broken implementation of freeze doesn't call @app.freeze def freeze return self if frozen? - + super end end @@ -52,15 +59,15 @@ class GoodMiddleware def initialize(app) @app = app end - + attr :app - + # Broken implementation of freeze doesn't call @app.freeze def freeze return self if frozen? - + @app.freeze - + super end end @@ -70,13 +77,13 @@ def initialize(app) @app = app @state = 0 end - + attr :app attr :state - + def call(env) @state += 1 - + return @app.call(env) end end diff --git a/spec/rack/freeze/builder_spec.rb b/spec/rack/freeze/builder_spec.rb index 7c0ba63..cf678dd 100644 --- a/spec/rack/freeze/builder_spec.rb +++ b/spec/rack/freeze/builder_spec.rb @@ -3,7 +3,7 @@ RSpec.describe StatefulMiddleware do include_context "middleware builder" - + it "should fail when invoked" do expect{app.call(env)}.to raise_error(RuntimeError, /can't modify frozen/) end @@ -18,11 +18,11 @@ Rack::Builder.new do use FaultyMiddleware use GoodMiddleware - + run proc{} end end - + it_behaves_like "frozen middleware" end @@ -35,7 +35,7 @@ use GoodMiddleware run proc{} end - + map '/bar' do use FaultyMiddleware use GoodMiddleware @@ -43,6 +43,17 @@ end end end - - it_behaves_like "frozen middleware" + + it "should be entirely frozen" do + current = builder.to_app + + expect(current).to_not be_nil + + # This can't traverse into Rack::URLMap and I'm not really sure how it should do that. + while current + expect(current).to be_frozen + + current = current.respond_to?(:app) ? current.app : nil + end + end end From 12594a0bcb1d90b866ac7e84414c82d556e54a95 Mon Sep 17 00:00:00 2001 From: Cyrille Courtiere Date: Thu, 21 Nov 2019 21:07:49 +0100 Subject: [PATCH 2/2] document how to ignore selected middlewares --- README.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f1ea617..25b568d 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,10 @@ class NonThreadSafeMiddleware @app = app @state = 0 end - + def call(env) @state += 1 - + return @app.call(env) end end @@ -69,16 +69,16 @@ class CacheEverythingForever @app = app @cache_all_the_things = Concurrent::Map.new end - + def freeze return self if frozen? - + # Don't freeze @cache_all_the_things @app.freeze - + super end - + def call(env) # Use the thread-safe `Concurrent::Map` to fetch the value or store it if it doesn't exist already. @cache_all_the_things.fetch_or_store(env[Rack::PATH_INFO]) do @@ -88,6 +88,16 @@ class CacheEverythingForever end ``` +### Can I ignore a specific middleware ? + +In some particular cases, we want to be able to ignore a mutable middleware. This can be done in `config.ru` : + +```ruby +Rack::Freeze.configure do |config| + config.ignored_middlewares << MutableButFineMiddleware +end +``` + ## Contributing 1. Fork it