Skip to content

Commit

Permalink
Added YARD docs (100% Yardstick coverage)
Browse files Browse the repository at this point in the history
  • Loading branch information
dkubb committed Oct 18, 2009
1 parent db49bad commit bbdea40
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 18 deletions.
10 changes: 8 additions & 2 deletions README.rdoc
@@ -1,9 +1,15 @@
= rack-conneg

Description goes here.
Negotiate the best static file for a request based on client preferences.

== Usage

Usage is the same as Rack::Static, eg:

use Rack::Conneg::Static, :urls => %w[ /css /images ], :root => 'public'

== Note on Patches/Pull Requests

* Fork the project.
* Make your feature addition or bug fix.
* Add tests for it. This is important so I don't break it in a
Expand Down
4 changes: 2 additions & 2 deletions Rakefile
Expand Up @@ -9,8 +9,8 @@ begin

Jeweler::Tasks.new do |gem|
gem.name = 'rack-conneg'
gem.summary = %Q{TODO: one-line summary of your gem}
gem.description = %Q{TODO: longer description of your gem}
gem.summary = 'Rack middleware for static file content negotiation'
gem.description = 'Negotiate the best static file for a request based on client preferences'
gem.email = 'dan.kubb@gmail.com'
gem.homepage = 'http://github.com/dkubb/rack-conneg'
gem.authors = [ 'Dan Kubb' ]
Expand Down
19 changes: 19 additions & 0 deletions lib/rack/conneg/file.rb
@@ -1,10 +1,29 @@
module Rack
class Conneg
class File

# Initialize a new File
#
# @param [#call] app
# the Rack app to handle static requests
#
# @return [File]
# returns a new File instance
#
# @api private
def initialize(file_server)
@file_server = file_server
end

# Negotiate a response using the request headers
#
# @param [Hash] env
# the Rack request environment
#
# @return [Array(Integer, Hash, #each)]
# returns a Rack response
#
# @api private
def call(env)
Negotiator.negotiate(@file_server, env)
end
Expand Down
98 changes: 92 additions & 6 deletions lib/rack/conneg/negotiator.rb
Expand Up @@ -7,56 +7,142 @@ class Negotiator
"406 Not Acceptable\n",
].freeze

# Negotiate a response using the request headers
#
# @param [#call] app
# the Rack app to handle static requests
# @param [Hash] env
# the Rack request environment
#
# @return [Array(Integer, Hash, #each)]
# returns a Rack response
#
# @api private
def self.negotiate(app, env)
negotiator = new(app, env)
negotiator.pass? ? negotiator.pass : negotiator.call
end

# Initialize a new Negotiator
#
# @param [#call] app
# the Rack app to handle static requests
# @param [Hash] env
# the Rack request environment
#
# @return [Negotiator]
# returns a new Negotiator instance
#
# @api private
def initialize(app, env)
@app = app
@env = env
@request = Acceptable::Request.new(@env)
@request = Acceptable::Request.new(env)
@path = Path.new(app.root, @request.path_info)
end

# Test if the request should be passed to the next Rack app
#
# @return [Boolean]
# true if the request should be passed through
#
# @api private
def pass?
not @path.variants?
end

def pass(env = @env)
# The response from the next Rack app
#
# @param [Hash] env
# optional Rack request environment
#
# @return [Array(Integer, Hash, #each)]
# returns a Rack response
#
# @api private
def pass(env = @request.env)
@app.call(env)
end

# Negotiate the optimal response based on client preferences
#
# @return [Array(Integer, Hash, #each)]
# returns a negotiated Rack response
#
# @api private
def call
variant = select_variant
response = variant ? serve_variant(variant.path_info) : NOT_ACCEPTABLE
response = variant ? serve_path(variant) : NOT_ACCEPTABLE
self.class.append_vary_header(response[1], 'Accept')
response
end

private

# Select the best variant based on client preferences
#
# @return [Path, nil]
# the negotiated variant, nil if none available
#
# @api private
def select_variant
variants = @path.variants
variants[@request.preferred_media_from(*variants.keys)]
end

def serve_variant(path_info)
status, headers, body = pass(@env.merge('PATH_INFO' => path_info))
# Serve the path to the client
#
# @param [Path] path
# the path to serve
#
# @return [Array(Integer, Hash, #each)]
# returns a negotiated Rack response
#
# @api private
def serve_path(path)
path_info = path.path_info
status, headers, body = pass(@request.env.merge('PATH_INFO' => path_info))
headers['Content-Location'] = path_info if (200..299).include?(status)
[ status, headers, body ]
end

# Append client header names used to negotiate the response to Vary
#
# @param [Hash] headers
# the Rack response headers
# @param [Array<String>] *names
# the client headers used to negotiate the response
#
# @return [undefined]
#
# @api private
def self.append_vary_header(headers, *names)
vary = split_header(headers['Vary'])
return if vary.include?('*')
headers['Vary'] = join_header(vary | names)
end

# Split the header value into an Array
#
# @param [#to_s] header
# the header value to split
#
# @return [Array<String>]
# the header values
#
# @api private
def self.split_header(header)
header.to_s.delete(' ').split(',')
end

# Join the header values into a String
#
# @param [Array<String>] values
# the header values to join
#
# @return [String]
# the header value
#
# @api private
def self.join_header(values)
values.join(',')
end
Expand Down
90 changes: 83 additions & 7 deletions lib/rack/conneg/path.rb
Expand Up @@ -5,26 +5,57 @@ class Conneg
class Path
EXTENSION_REGEXP = Regexp.union(*Mime::MIME_TYPES.keys.map { |ext| ext[1..-1] }).freeze

# Return the request path
#
# @return [String]
# the request path
#
# @api private
attr_reader :path_info

# Initialize a new Path
#
# @param [String, Pathname] root
# the server document root
# @param [String] path_info
# the request path
#
# @return [Path]
# returns a new Path instance
#
# @api private
def initialize(root, path_info)
@root = Pathname(root)
@path_info = self.class.normalize_path_info(path_info)
@path = @root + @path_info[1..-1]
end

# Return the mime type for the request path
#
# @return [String, nil]
# the mime type if known
#
# @api private
def mime_type
Mime.mime_type(extname, nil)
end

def exist?
@path.exist?
end

# Test if there are variants for the request path
#
# @return [Boolean]
# true if there are variants
#
# @api private
def variants?
variants.any?
end

# Return the Hash of media types and variants
#
# @return [Hash]
# the variants for the request path
#
# @api private
def variants
return @variants if @variants
variants = {}
Expand All @@ -34,34 +65,79 @@ def variants

private

# Return the variant paths
#
# @return [Array<Path>]
# the variant paths
#
# @api private
def variant_paths
paths.select { |path| variant?(path) && path.exist? }
rescue SystemCallError
[]
paths.select { |path| variant?(path) }
end

# Return all the paths in the request path directory
#
# @return [Array<Path>]
# the paths in the current directory
#
# @api private
def paths
directory.map do |path|
self.class.new(@root, path.relative_path_from(@root))
end
end

# Return all the children in the request path directory
#
# @return [Array<Pathname>]
# the children in the current directory
#
# @api private
def directory
@path.dirname.children
rescue SystemCallError
[]
end

# Return the request path extension
#
# @return [String, nil]
# the request path extension
#
# @api private
def extname
@path.extname
end

# Test if the other Path is a variant
#
# @param [Path] other
# the other Path to test
#
# @return [Boolean]
# true if the other Path is a variant
#
# @api private
def variant?(other)
pattern === other.path_info
end

# Return a Regexp to match the request path and known extensions
#
# @return [Regexp]
# match the request path
#
# @api private
def pattern
@pattern ||= /\A#{Regexp.escape(path_info)}\.#{EXTENSION_REGEXP}\z/.freeze
end

# Normalize the request path info
#
# @return [String]
# the request path
#
# @api private
def self.normalize_path_info(path_info)
"/#{Utils.unescape(path_info.to_s).gsub(/\A\//, '')}"
end
Expand Down
17 changes: 16 additions & 1 deletion lib/rack/conneg/static.rb
@@ -1,7 +1,22 @@
module Rack
class Conneg
class Static < Rack::Static
def initialize(*)

# Initialize middleware for static file content negotiation
#
# @example
# use Rack::Conneg::Static, :urls => %w[ /css /images ], :root => 'public'
#
# @param [#call] app
# the Rack app to handle static requests
# @param [Hash] options
# optional middleware configuration
#
# @return [Static]
# returns a new Static instance
#
# @api public
def initialize(app, options = {})
super
@file_server = File.new(@file_server)
end
Expand Down

0 comments on commit bbdea40

Please sign in to comment.