rack-info 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +82 -0
- data/Rakefile +1 -0
- data/TODO.txt +4 -0
- data/examples/basic_example.ru +7 -0
- data/examples/html_comment_example.ru +10 -0
- data/examples/html_meta_tag_example.ru +12 -0
- data/examples/path_example.ru +9 -0
- data/lib/rack-info.rb +1 -0
- data/lib/rack/info.rb +67 -0
- data/lib/rack/info/config.rb +57 -0
- data/lib/rack/info/html_comment.rb +26 -0
- data/lib/rack/info/html_formatter.rb +7 -0
- data/lib/rack/info/html_meta_tag.rb +17 -0
- data/lib/rack/info/version.rb +5 -0
- data/rack-info.gemspec +27 -0
- data/spec/config_spec.rb +59 -0
- data/spec/html_comment_spec.rb +58 -0
- data/spec/html_meta_tag_spec.rb +27 -0
- data/spec/info_spec.rb +207 -0
- data/spec/spec_helper.rb +60 -0
- data/spec/test_examples.rb +55 -0
- metadata +162 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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`
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/TODO.txt
ADDED
@@ -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,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"]] }
|
data/lib/rack-info.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'rack/info'
|
data/lib/rack/info.rb
ADDED
@@ -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,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
|
data/rack-info.gemspec
ADDED
@@ -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
|
data/spec/config_spec.rb
ADDED
@@ -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('"it's & its"')
|
24
|
+
tag.should include('my <item>')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/spec/info_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|