rack-info 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rack-info.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Ryan Greenberg
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,82 @@
1
+ # Rack::Info
2
+
3
+ `Rack::Info` is a Rack middleware that can be used to add information about your application or environment to requests. You can use it to expose data like the current version of the application or which host served the request.
4
+
5
+ This information can be added as X-headers, output as HTML, or served from a dedicated endpoint.
6
+
7
+ ## Installation
8
+
9
+ ```
10
+ gem install rack-info
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ For the simple case where you want to add the same values as headers to every request, provide `Rack::Info` with a hash of key/value pairs.
16
+
17
+ Here's an example from `examples/basic_example.ru`:
18
+ ```
19
+ require 'rack/info'
20
+ require 'socket'
21
+
22
+ use Rack::Head
23
+ use Rack::Info, {:git => `git rev-parse HEAD`.strip, :host => Socket.gethostname}
24
+ run lambda {|env| [200, {"Content-Type" => "text/plain"}, ["OK"]] }
25
+ ```
26
+
27
+ After you start a server by running `rackup config/basic_example.ru`, you can see the headers are added to your request:
28
+
29
+ ```
30
+ $ curl -I http://localhost:9292
31
+ HTTP/1.1 200 OK
32
+ X-Git: 3e534f6302eca4e8f94456efa09523b49b1c41c7
33
+ X-Host: Ollantaytambo.local
34
+ Transfer-Encoding: chunked
35
+ Connection: close
36
+ ```
37
+
38
+ For more complex cases, use a `Rack::Info::Config` object:
39
+
40
+ ```
41
+ use(Rack::Info, Rack::Info::Config.new do |config|
42
+ # Set any desired options; see Configuration below
43
+ config.metadata = {:git => `git rev-parse HEAD`.strip, :host => Socket.gethostname}
44
+ config.is_enabled = lambda {|env| [true, false].sample }
45
+ config.path = "/version"
46
+ end)
47
+ ```
48
+
49
+ ## Configuration
50
+
51
+ Configuration is done via an instance of `Rack::Info::Config`. You can create a configuration by providing a block to the constructor, or by setting values directly on a new instance:
52
+
53
+ ```
54
+ Rack::Info::Config.new do |config|
55
+ config.add_html = false
56
+ end
57
+
58
+ config = Rack::Info::Config.new
59
+ config.add_html = false
60
+ ```
61
+
62
+ The following options can be set on an config object:
63
+
64
+ - `metadata`: a hash of key/value pairs that will be added as X-headers, HTML content, or exposed directly at a JSON endpoint, depending on the other configuration. (default: `{}`)
65
+ - `is_enabled`: whether or not this middleware will add any metadata to the response. It can be a boolean value, or an object that responds to .call with a boolean value. The Rack request environment is provided to a callable object. This can be useful for adding data only to requests from a certain IP block, for example. (default: `true`)
66
+ - `add_headers`: whether or not metadata will be added to this request as X-headers. It can be a boolean value, or an object that responds to .call with a boolean value. The Rack request environment _and_ current Rack response tuple are provided to a callable object. (default: `true`)
67
+ - `add_html`: whether or not metadata will be added to this request as HTML. It can be a boolean value, or an object that responds to .call with a boolean value. The Rack request environment *and* current Rack response tuple are provided to a callable object. Note: content is only added to responses with a content-type header of text/html. (default: `true`)
68
+ - `insert_html_after`: the HTML tag after which the HTML metadata will be added. (default: `</body>`)
69
+ - `html_formatter`: object that converts metadata pairs to an HTML string. See `HTMLComment` and `HTMLMetaTag` for examples. (default: `HTMLComment`)
70
+ - `path`: an endpoint at which metadata will be returned as a JSON string. Set to nil to disable. (default: `nil`)
71
+
72
+ ## Examples
73
+
74
+ You can find examples of different configurations in `examples`.
75
+
76
+ ## Development
77
+
78
+ To make changes to rack-metadata:
79
+
80
+ 1. Clone this repository
81
+ 2. `bundle install`
82
+ 3. Run tests with `rspec`
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,4 @@
1
+ - Ensure that HTTP headers are properly sanitized (see http://tools.ietf.org/html/rfc2616#section-4.2, http://tools.ietf.org/html/rfc2616#section-6.2, http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html)
2
+ - Allow metadata to have dynamic values when values are callable
3
+ - Cache HTML content and header content when metadata values are not dynamic
4
+ - Have the config validate itself, and raise an exception in ::Metadata if config is invalid
@@ -0,0 +1,7 @@
1
+ $:.unshift(File.expand_path('../../lib', __FILE__))
2
+ require 'rack/info'
3
+ require 'socket'
4
+
5
+ use Rack::Head
6
+ use Rack::Info, {:git => `git rev-parse HEAD`.strip, :host => Socket.gethostname}
7
+ run lambda {|env| [200, {"Content-Type" => "text/plain"}, ["OK"]] }
@@ -0,0 +1,10 @@
1
+ $:.unshift(File.expand_path('../../lib', __FILE__))
2
+ require 'rack/info'
3
+ require 'socket'
4
+
5
+ HTML = "<html><head><title>My Website</title></head><body>My content</body></html>".freeze
6
+
7
+ use(Rack::Info, Rack::Info::Config.new do |config|
8
+ config.data = {:git => `git rev-parse HEAD`.strip, :host => Socket.gethostname}
9
+ end)
10
+ run lambda {|env| [200, {"Content-Type" => "text/html"}, [HTML]] }
@@ -0,0 +1,12 @@
1
+ $:.unshift(File.expand_path('../../lib', __FILE__))
2
+ require 'rack/info'
3
+ require 'socket'
4
+
5
+ HTML = "<html><head><title>My Website</title></head><body>My content</body></html>".freeze
6
+
7
+ use(Rack::Info, Rack::Info::Config.new do |config|
8
+ config.data = {:git => `git rev-parse HEAD`.strip, :host => Socket.gethostname}
9
+ config.insert_html_after = '<head>'
10
+ config.html_formatter = Rack::Info::HTMLMetaTag
11
+ end)
12
+ run lambda {|env| [200, {"Content-Type" => "text/html"}, [HTML]] }
@@ -0,0 +1,9 @@
1
+ $:.unshift(File.expand_path('../../lib', __FILE__))
2
+ require 'rack/info'
3
+ require 'socket'
4
+
5
+ use(Rack::Info, Rack::Info::Config.new do |config|
6
+ config.data = {:git => `git rev-parse HEAD`, :host => Socket.gethostname}
7
+ config.path = "/server_info"
8
+ end)
9
+ run lambda {|env| [200, {}, ["OK"]] }
@@ -0,0 +1 @@
1
+ require 'rack/info'
@@ -0,0 +1,67 @@
1
+ require "multi_json"
2
+
3
+ require "rack/info/version"
4
+ require "rack/info/config"
5
+ require "rack/info/html_formatter"
6
+ require "rack/info/html_comment"
7
+ require "rack/info/html_meta_tag"
8
+
9
+ module Rack
10
+ class Info
11
+ CONTENT_TYPE_HEADER = 'Content-Type'
12
+ HTML_CONTENT_TYPE = 'text/html'
13
+
14
+ def self.header_name(str)
15
+ "X-" + str.to_s.split(/[-_ ]/).map(&:capitalize).join("-")
16
+ end
17
+
18
+ def self.header_value(obj)
19
+ obj.to_s
20
+ end
21
+
22
+ attr_reader :app, :config, :metadata
23
+
24
+ def initialize(app, hsh_or_config = {})
25
+ @app = app
26
+ @config = Config.from(hsh_or_config)
27
+ @data_headers = to_headers(@config.data)
28
+ end
29
+
30
+ def call(env)
31
+ return app.call(env) unless config.enabled?(env)
32
+ return json_rsp if config.path == env["PATH_INFO"]
33
+
34
+ status, headers, body = @app.call(env)
35
+ headers.merge!(@data_headers) if config.add_headers?(env, [status, headers, body])
36
+ if html?(headers) && config.add_html?(env, [status, headers, body])
37
+ body = add_html(body)
38
+ end
39
+
40
+ [status, headers, body]
41
+ end
42
+
43
+ private
44
+
45
+ def to_headers(hsh)
46
+ Hash[@config.data.map do |k, v|
47
+ [self.class.header_name(k), self.class.header_value(v)]
48
+ end]
49
+ end
50
+
51
+ def json_rsp
52
+ [200, {"Content-Type" => "application/json"}, [MultiJson.dump(config.data)]]
53
+ end
54
+
55
+ def html?(headers)
56
+ headers[CONTENT_TYPE_HEADER] &&
57
+ headers[CONTENT_TYPE_HEADER].start_with?(HTML_CONTENT_TYPE)
58
+ end
59
+
60
+ def add_html(body)
61
+ content = ""
62
+ body.each {|ea| content << ea}
63
+ new_html_content = config.html_formatter.format(config.data)
64
+ [ content.sub(config.insert_html_after) {|match| match + new_html_content } ]
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,57 @@
1
+ class Rack::Info
2
+ class Config
3
+ # You can create a configuration by providing a block to the constructor,
4
+ # or by setting values directly on a new instance:
5
+ #
6
+ # Rack::Info::Config.new do |config|
7
+ # config.add_html = false
8
+ # end
9
+ #
10
+ # config = Rack::Info::Config.new
11
+ # config.add_html = false
12
+ #
13
+ # Configuration options (see README for explanation of options)
14
+ # - data
15
+ # - is_enabled
16
+ # - add_headers
17
+ # - add_html
18
+ # - html_formatter
19
+ # - insert_html_after
20
+ # - path
21
+ attr_accessor :data, :is_enabled, :add_headers, :add_html,
22
+ :html_formatter, :insert_html_after, :path
23
+
24
+ def self.from(obj)
25
+ obj.is_a?(self) ? obj : self.new {|cnf| cnf.data = obj }
26
+ end
27
+
28
+ def initialize
29
+ set_defaults
30
+ yield self if block_given?
31
+ end
32
+
33
+ def enabled?(env)
34
+ is_enabled.respond_to?(:call) ? is_enabled.call(env) : is_enabled
35
+ end
36
+
37
+ def add_headers?(env, rsp)
38
+ add_headers.respond_to?(:call) ? add_headers.call(env, rsp) : add_headers
39
+ end
40
+
41
+ def add_html?(env, rsp)
42
+ add_html.respond_to?(:call) ? add_html.call(env, rsp) : add_html
43
+ end
44
+
45
+ private
46
+
47
+ def set_defaults
48
+ self.data = {}
49
+ self.is_enabled = true
50
+ self.add_headers = true
51
+ self.add_html = true
52
+ self.html_formatter = HTMLComment
53
+ self.insert_html_after = '</body>'
54
+ self.path = nil
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,26 @@
1
+ class Rack::Info
2
+ # See http://www.w3.org/TR/html-markup/spec.html#comments and
3
+ # http://www.w3.org/TR/html5/syntax.html#comments for restrictions on
4
+ # HTML comments.
5
+ class HTMLComment < HTMLFormatter
6
+ START_COMMENT = "<!--"
7
+ END_COMMENT = "-->"
8
+ INVALID_COMMENT_CONTENT = "--"
9
+
10
+ def self.format(hsh)
11
+ START_COMMENT + "\n" + sanitize(format_pairs(hsh)) + "\n" + END_COMMENT
12
+ end
13
+
14
+ def self.sanitize(str)
15
+ str.gsub(INVALID_COMMENT_CONTENT, '')
16
+ end
17
+
18
+ def self.format_pairs(hsh)
19
+ hsh.map {|k,v| format_pair(k, v) }.sort_by {|k, v| k }.join("\n")
20
+ end
21
+
22
+ def self.format_pair(key, value)
23
+ "#{key}: #{value}"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ class Rack::Info
2
+ class HTMLFormatter
3
+ def self.format(hsh)
4
+ raise NotImplementedError
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ class Rack::Info
2
+ class HTMLMetaTag < HTMLFormatter
3
+ def self.format(hsh)
4
+ "\n" + hsh.map {|k, v| format_item(k, v) }.join("\n") + "\n"
5
+ end
6
+
7
+ def self.format_item(key, value)
8
+ %|<meta name="#{h(key)}" content="#{h(value)}">|
9
+ end
10
+
11
+ private
12
+
13
+ def self.h(str)
14
+ Rack::Utils.escape_html(str)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ module Rack
2
+ class Info
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rack/info/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rack-info"
8
+ spec.version = Rack::Info::VERSION
9
+ spec.authors = ["Ryan Greenberg"]
10
+ spec.email = ["ryangreenberg@gmail.com"]
11
+ spec.summary = "Rack middleware to add information to request headers or body"
12
+ spec.description = spec.summary
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'multi_json', '~> 1.0'
22
+ spec.add_dependency 'rack'
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.3"
25
+ spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "rspec", "~> 2.14.1"
27
+ end
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rack::Info::Config do
4
+ it "passes itself to a constructor block" do
5
+ expect do |blk|
6
+ Rack::Info::Config.new(&blk)
7
+ end.to yield_with_args(Rack::Info::Config)
8
+ end
9
+
10
+ describe ".from" do
11
+ it "returns Config objects unchanged" do
12
+ config = Rack::Info::Config.new
13
+ Rack::Info::Config.from(config).should == config
14
+ end
15
+
16
+ it "creates a new Config object from a hash" do
17
+ hsh = {:key => :value}
18
+ new_config = Rack::Info::Config.from(hsh)
19
+ new_config.data.should == hsh
20
+ end
21
+ end
22
+
23
+ describe "add_headers?" do
24
+ before :each do
25
+ @rack_env = {}
26
+ @rack_rsp = [200, {}, [""]]
27
+ end
28
+
29
+ it "is true when add_headers is set to true" do
30
+ config = Rack::Info::Config.new
31
+ config.add_headers = true
32
+ config.should be_add_headers(@rack_env, @rack_rsp)
33
+ end
34
+
35
+ it "is false when add_headers is set to false" do
36
+ config = Rack::Info::Config.new
37
+ config.add_headers = false
38
+ config.should_not be_add_headers(@rack_env, @rack_rsp)
39
+ end
40
+
41
+ context "when add_headers is callable" do
42
+ it "calls add_headers" do
43
+ config = Rack::Info::Config.new
44
+ lambda_was_called = false
45
+ config.add_headers = lambda {|*args| lambda_was_called = true }
46
+ config.add_headers?(@rack_env, @rack_rsp)
47
+ lambda_was_called.should be_true
48
+ end
49
+
50
+ it "provides the rack_env and response" do
51
+ config = Rack::Info::Config.new
52
+ yielded_args = []
53
+ config.add_headers = lambda {|*args| yielded_args = args }
54
+ config.add_headers?(@rack_env, @rack_rsp)
55
+ yielded_args.should == [@rack_env, @rack_rsp]
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rack::Info::HTMLComment do
4
+ describe ".format" do
5
+ before :each do
6
+ @hsh = {:some_key => :some_value}
7
+ end
8
+
9
+ it "returns a string that starts with <!--" do
10
+ Rack::Info::HTMLComment.format(@hsh).should be_start_with("<!--")
11
+ end
12
+
13
+ it "returns a string that ends with -->" do
14
+ Rack::Info::HTMLComment.format(@hsh).should be_end_with("-->")
15
+ end
16
+
17
+ it "returns a string that includes the formatted values" do
18
+ comment = Rack::Info::HTMLComment.format(@hsh)
19
+ comment.should include(Rack::Info::HTMLComment.format_pairs(@hsh))
20
+ end
21
+
22
+ it "removes the string -- to avoid closing the comment early" do
23
+ @hsh["value_including_html"] = "attempt to close comment early -->"
24
+ comment = Rack::Info::HTMLComment.format(@hsh)
25
+ comment.should include("attempt to close comment early >")
26
+ end
27
+ end
28
+
29
+ describe ".format_pairs" do
30
+ it "separates individual pairs with a newline" do
31
+ hsh = {:a => 1, :b => 2}
32
+ output = Rack::Info::HTMLComment.format_pairs(hsh)
33
+ output.should include("\n")
34
+ end
35
+
36
+ it "sorts pairs alphabetically by key name" do
37
+ hsh = {:c => 3, :b => 2, :a => 1}
38
+ alphabetical_order = [[:a, 1], [:b, 2], [:c, 3]]
39
+ expected_order = alphabetical_order.map do |ea|
40
+ Rack::Info::HTMLComment.format_pair(*ea)
41
+ end
42
+ output = Rack::Info::HTMLComment.format_pairs(hsh)
43
+ output.split("\n").should == expected_order
44
+ end
45
+ end
46
+
47
+ describe ".format_pair" do
48
+ it "separates the key and value with a colon" do
49
+ output = Rack::Info::HTMLComment.format_pair("some_key", "some_value")
50
+ output.should == "some_key: some_value"
51
+ end
52
+
53
+ it "converts key and value to strings" do
54
+ output = Rack::Info::HTMLComment.format_pair(:some_key, :some_value)
55
+ output.should == "some_key: some_value"
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rack::Info::HTMLMetaTag do
4
+ describe ".format_item" do
5
+ it "returns a <meta> tag string" do
6
+ tag = Rack::Info::HTMLMetaTag.format_item("color", "red")
7
+ tag.should be_start_with("<meta")
8
+ tag.should be_end_with(">")
9
+ end
10
+
11
+ it "converts the key into a name attribute" do
12
+ tag = Rack::Info::HTMLMetaTag.format_item("color", "red")
13
+ tag.should include('name="color"')
14
+ end
15
+
16
+ it "converts the value into a content attribute" do
17
+ tag = Rack::Info::HTMLMetaTag.format_item("color", "red")
18
+ tag.should include('content="red"')
19
+ end
20
+
21
+ it "escapes HTML entities in the key and value" do
22
+ tag = Rack::Info::HTMLMetaTag.format_item(%|"it's & its"|, "my <item>")
23
+ tag.should include('&quot;it&#x27;s &amp; its&quot;')
24
+ tag.should include('my &lt;item&gt;')
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,207 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rack::Info do
4
+ def base_data
5
+ {:some_key => :some_value}
6
+ end
7
+
8
+ def base_config(hsh = base_data)
9
+ Rack::Info::Config.from(hsh)
10
+ end
11
+
12
+ describe ".header_name" do
13
+ it "prepends X-" do
14
+ Rack::Info.header_name("Dog").should == "X-Dog"
15
+ end
16
+
17
+ it "capitalizes words" do
18
+ Rack::Info.header_name("cat").should == "X-Cat"
19
+ end
20
+
21
+ it "converts symbols to strings" do
22
+ Rack::Info.header_name(:giraffe).should == "X-Giraffe"
23
+ end
24
+
25
+ it "converts spaces to dashes" do
26
+ Rack::Info.header_name("mountain lion").should == "X-Mountain-Lion"
27
+ end
28
+
29
+ it "converts underscores to dashes" do
30
+ Rack::Info.header_name("mountain_lion").should == "X-Mountain-Lion"
31
+ end
32
+ end
33
+
34
+ describe ".header_value" do
35
+ it "converts to a string" do
36
+ actual_values = [1, true, :true, nil]
37
+ expected_values = ["1", "true", "true", ""]
38
+ actual_values.zip(expected_values).each do |actual, expected|
39
+ Rack::Info.header_value(actual).should == expected
40
+ end
41
+ end
42
+ end
43
+
44
+ describe "#call" do
45
+ it "can be constructed with a hash instead of a config object" do
46
+ hsh = {:color => "Red", :virtue => "Beauty"}
47
+ app = rack_app(OK_APP, Rack::Info, hsh)
48
+ rsp = app.call(rack_env)
49
+ rsp.headers.should include({"X-Color" => "Red"}, {"X-Virtue" => "Beauty"})
50
+ end
51
+
52
+ context "when config.enabled? returns false" do
53
+ before :each do
54
+ @config = base_config
55
+ @config.stub(:enabled?).and_return(false)
56
+ end
57
+
58
+ it "does not add headers" do
59
+ env = rack_env
60
+ @config.stub(:add_headers?).and_return(true)
61
+ app = rack_app(OK_APP, Rack::Info, @config)
62
+ rsp = app.call(env)
63
+ rsp.headers.should == unchanged_rsp(OK_APP, env).headers
64
+ end
65
+
66
+ it "calls the underlying app even if config.path is requested" do
67
+ env = rack_env
68
+ @config.path = env["PATH_INFO"]
69
+ app = rack_app(NOT_FOUND_APP, Rack::Info, @config)
70
+ rsp = app.call(env)
71
+ rsp.headers.should == unchanged_rsp(NOT_FOUND_APP, env).headers
72
+ end
73
+
74
+ it "does not modify HTML" do
75
+ env = rack_env
76
+ app = rack_app(HTML_APP, Rack::Info, @config)
77
+ rsp = app.call(env)
78
+ rsp.body.should == unchanged_rsp(HTML_APP, env).body
79
+ end
80
+ end
81
+
82
+ context "when config.add_headers? returns true" do
83
+ it "adds the data as response headers" do
84
+ config = Rack::Info::Config.new do |config|
85
+ config.data = {:color => "Blue", :virtue => "Justice"}
86
+ end
87
+ config.stub(:add_headers?).and_return(true)
88
+ app = rack_app(OK_APP, Rack::Info, config)
89
+ rsp = app.call(rack_env)
90
+ rsp.headers.should include({"X-Color" => "Blue"}, {"X-Virtue" => "Justice"})
91
+ end
92
+ end
93
+
94
+ context "when config.add_headers? returns false" do
95
+ it "does not modify the response headers" do
96
+ env = rack_env
97
+ config = base_config
98
+ config.stub(:add_headers?).and_return(false)
99
+ app = rack_app(OK_APP, Rack::Info, config)
100
+ rsp = app.call(env)
101
+ rsp.headers.should == unchanged_rsp(OK_APP, env).headers
102
+ end
103
+ end
104
+
105
+ context "when config.add_html? returns true" do
106
+ before :each do
107
+ @config = base_config
108
+ @config.stub(:add_html?).and_return(true)
109
+ @config.html_formatter = Rack::Info::HTMLComment
110
+ @env = rack_env
111
+ end
112
+
113
+ it "adds an HTML fragment when the response Content-Type is text/html" do
114
+ app = rack_app(HTML_APP, Rack::Info, @config)
115
+ rsp = app.call(@env)
116
+ rsp.headers["Content-Type"].should == "text/html"
117
+ rsp.body.should include Rack::Info::HTMLComment.format(@config.data)
118
+ end
119
+
120
+ it "adds an HTML fragment when the response Content-Type is 'text/html; charset=utf-8'" do
121
+ charset_app = lambda do |env|
122
+ HTML_APP.call(env).tap {|s, h, b| h.merge!('Content-Type' => 'text/html; charset=utf-8') }
123
+ end
124
+ app = rack_app(charset_app, Rack::Info, @config)
125
+ rsp = app.call(@env)
126
+ rsp.body.should include Rack::Info::HTMLComment.format(@config.data)
127
+ end
128
+
129
+ it "puts the HTML fragment after config.insert_html_after" do
130
+ @config.insert_html_after = "<html>"
131
+ app = rack_app(HTML_APP, Rack::Info, @config)
132
+ rsp = app.call(@env)
133
+ rsp.body.should include("<html>" + Rack::Info::HTMLComment.format(@config.data))
134
+ end
135
+
136
+ it "puts the HTML fragment after config.insert_html_after as a regex" do
137
+ @config.insert_html_after = /<head.*?>/
138
+ app = rack_app(HTML_APP, Rack::Info, @config)
139
+ rsp = app.call(@env)
140
+ rsp.body.should include("<head>" + Rack::Info::HTMLComment.format(@config.data))
141
+ end
142
+
143
+ it "uses the HTML fragment provided by config.html_formatter" do
144
+ formatter = double("formatter")
145
+ allow(formatter).to receive(:format).and_return("<strong>content</strong>")
146
+ @config.html_formatter = formatter
147
+ app = rack_app(HTML_APP, Rack::Info, @config)
148
+ rsp = app.call(@env)
149
+ rsp.body.should include("<strong>content</strong>")
150
+ end
151
+
152
+ it "provides config.data when calling config.html_formatter" do
153
+ formatter = double("formatter", :format => "")
154
+ @config.html_formatter = formatter
155
+ app = rack_app(HTML_APP, Rack::Info, @config)
156
+ rsp = app.call(@env)
157
+ expect(formatter).to have_received(:format).with(@config.data)
158
+ end
159
+
160
+ it "does not error if Content-Type is not provided" do
161
+ malformed_app = lambda {|env| [200, {}, ["OK"]] }
162
+ app = rack_app(malformed_app, Rack::Info, @config)
163
+ rsp = app.call(@env)
164
+ rsp.body.should == unchanged_rsp(malformed_app, @env).body
165
+ end
166
+
167
+ it "does not modify the response body when Content-Type is not text/html" do
168
+ app = rack_app(OK_APP, Rack::Info, @config)
169
+ rsp = app.call(@env)
170
+ rsp.headers["Content-Type"].should_not == "text/html"
171
+ rsp.body.should == unchanged_rsp(OK_APP, @env).body
172
+ end
173
+ end
174
+
175
+ context "when config.path matches the request path" do
176
+ before :each do
177
+ @config = base_config
178
+ @config.path = "/version"
179
+ @env = rack_env(@config.path)
180
+ end
181
+
182
+ it "does not call the underlying app" do
183
+ uncalled_app = lambda {|env| raise RuntimeError, "Underlying app should not be called" }
184
+ app = rack_app(uncalled_app, Rack::Info, @config)
185
+ lambda { app.call(@env) }.should_not raise_error
186
+ end
187
+
188
+ it "returns HTTP 200" do
189
+ app = rack_app(NOT_FOUND_APP, Rack::Info, @config)
190
+ rsp = app.call(@env)
191
+ rsp.status.should == 200
192
+ end
193
+
194
+ it "sets the Content-Type to application/json" do
195
+ app = rack_app(NOT_FOUND_APP, Rack::Info, @config)
196
+ rsp = app.call(@env)
197
+ rsp.headers["Content-Type"].should == "application/json"
198
+ end
199
+
200
+ it "returns config.data as a JSON string" do
201
+ app = rack_app(NOT_FOUND_APP, Rack::Info, @config)
202
+ rsp = app.call(@env)
203
+ rsp.body.should == '{"some_key":"some_value"}'
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,60 @@
1
+ $:.unshift('../lib')
2
+ require 'rack/info'
3
+
4
+ require 'rack'
5
+ require 'rack/builder'
6
+
7
+ require 'rspec'
8
+ require 'rspec/autorun'
9
+
10
+ HTML =<<MARKUP
11
+ <!DOCTYPE html>
12
+ <html>
13
+ <head>
14
+ <title>The Internet</html>
15
+ </head>
16
+ <body>
17
+ <h1>The Internet</h1>
18
+ <p>Welcome</p>
19
+ </body>
20
+ </html>
21
+ MARKUP
22
+
23
+ # Simple Rack apps for testing
24
+ OK_APP = lambda {|env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
25
+ NOT_FOUND_APP = lambda {|env| [400, {'Content-Type' => 'text/plain'}, ['Not found']] }
26
+ HTML_APP = lambda {|env| [200, {'Content-Type' => 'text/html'}, [HTML]] }
27
+
28
+ module RackSpecHelpers
29
+ def rack_env(path="/")
30
+ Rack::MockRequest.env_for(path)
31
+ end
32
+
33
+ # Construct a middleware chain with +underlying_app+ at the bottom,
34
+ # Rack::Lint on either side of +middleware+, and +middleware_args+
35
+ # provided when constructing +middleware+.
36
+ #
37
+ # For more convenient assertions, the response is automatically
38
+ # wrapped as a Rack::MockResponse instead of a [status, header, body]
39
+ # tuple.
40
+ def rack_app(underlying_app, middleware, *middleware_args)
41
+ app = Rack::Builder.new do
42
+ use Rack::Lint
43
+ use middleware, *middleware_args
44
+ use Rack::Lint
45
+ run underlying_app
46
+ end.to_app
47
+
48
+ lambda do |env|
49
+ Rack::MockResponse.new(*app.call(env))
50
+ end
51
+ end
52
+
53
+ def unchanged_rsp(app, env)
54
+ Rack::MockResponse.new(*app.call(env))
55
+ end
56
+ end
57
+
58
+ RSpec.configure do |config|
59
+ config.include RackSpecHelpers
60
+ end
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ # Ensure that all of the examples start properly and respond to a request
4
+ # successfully
5
+ #
6
+ # This set of tests relies on certain Unix commands that will not be available
7
+ # on Windows.
8
+
9
+ require 'fileutils'
10
+ require 'net/http'
11
+ require 'tmpdir'
12
+
13
+ def wait_for_port(port, timeout_secs)
14
+ timeout_secs.times do
15
+ success = system("lsof -i:#{port} >/dev/null")
16
+ return if success
17
+ sleep 1
18
+ end
19
+ abort("Port #{port} was not in use after waiting for #{timeout_secs} seconds")
20
+ end
21
+
22
+ def with_server(rackup_config)
23
+ pid_file = "#{Dir.tmpdir}/example.pid"
24
+ port = 9292
25
+ print "Starting server using #{rackup_config}..."
26
+ success = system("rackup --daemonize --pid #{pid_file} #{rackup_config}")
27
+ abort("Unable to start server using #{example}") unless success
28
+ wait_for_port(port, 10)
29
+ puts "OK"
30
+
31
+ yield URI.parse("http://localhost:#{port}") if block_given?
32
+ ensure
33
+ pid = File.read(pid_file)
34
+ puts "Stopping pid #{pid}"
35
+ system("kill -9 #{pid}")
36
+ end
37
+
38
+ def main
39
+ examples = Dir["./examples/*.ru"]
40
+ examples.each do |example|
41
+ with_server(example) do |uri|
42
+ http = Net::HTTP.new(uri.host, uri.port)
43
+ http.request_get("/") do |rsp|
44
+ puts ""
45
+ puts "GET #{uri}/ => HTTP #{rsp.code}"
46
+ rsp.each_header {|header| puts "#{header}: #{rsp[header]}" }
47
+ puts rsp.body
48
+ puts ""
49
+ abort("Received non-HTTP 200 response from #{uri}") unless rsp.code == "200"
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ main
metadata ADDED
@@ -0,0 +1,162 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-info
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ryan Greenberg
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-12-05 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: multi_json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rack
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: bundler
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '1.3'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rake
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 2.14.1
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 2.14.1
94
+ description: Rack middleware to add information to request headers or body
95
+ email:
96
+ - ryangreenberg@gmail.com
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - .gitignore
102
+ - Gemfile
103
+ - LICENSE.txt
104
+ - README.md
105
+ - Rakefile
106
+ - TODO.txt
107
+ - examples/basic_example.ru
108
+ - examples/html_comment_example.ru
109
+ - examples/html_meta_tag_example.ru
110
+ - examples/path_example.ru
111
+ - lib/rack-info.rb
112
+ - lib/rack/info.rb
113
+ - lib/rack/info/config.rb
114
+ - lib/rack/info/html_comment.rb
115
+ - lib/rack/info/html_formatter.rb
116
+ - lib/rack/info/html_meta_tag.rb
117
+ - lib/rack/info/version.rb
118
+ - rack-info.gemspec
119
+ - spec/config_spec.rb
120
+ - spec/html_comment_spec.rb
121
+ - spec/html_meta_tag_spec.rb
122
+ - spec/info_spec.rb
123
+ - spec/spec_helper.rb
124
+ - spec/test_examples.rb
125
+ homepage: ''
126
+ licenses:
127
+ - MIT
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ none: false
134
+ requirements:
135
+ - - ! '>='
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ segments:
139
+ - 0
140
+ hash: 4600296762285921712
141
+ required_rubygems_version: !ruby/object:Gem::Requirement
142
+ none: false
143
+ requirements:
144
+ - - ! '>='
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ segments:
148
+ - 0
149
+ hash: 4600296762285921712
150
+ requirements: []
151
+ rubyforge_project:
152
+ rubygems_version: 1.8.23
153
+ signing_key:
154
+ specification_version: 3
155
+ summary: Rack middleware to add information to request headers or body
156
+ test_files:
157
+ - spec/config_spec.rb
158
+ - spec/html_comment_spec.rb
159
+ - spec/html_meta_tag_spec.rb
160
+ - spec/info_spec.rb
161
+ - spec/spec_helper.rb
162
+ - spec/test_examples.rb