rack-conneg 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README.rdoc +38 -0
  2. data/lib/rack/conneg.rb +149 -0
  3. metadata +66 -0
data/README.rdoc ADDED
@@ -0,0 +1,38 @@
1
+ = Description
2
+ Content negotiation for Rack applications, including a Rails-style respond_to block.
3
+
4
+ = Features
5
+
6
+ * Allows both file-extension-based and Accept: header-based content negotiation
7
+ * Allows certain routes to pass through without negotiation (useful for static files, etc.)
8
+ * Falls back to a pre-set type if negotiation fails
9
+ * Injects a respond_to method with default handler into the application's namespace
10
+ * Injects negotiated_type and negotiated_ext methods both into the application and into Rack::Request
11
+
12
+ = Sinatra Example
13
+
14
+ require 'sinatra'
15
+ require 'rack/conneg'
16
+
17
+ use(Rack::Conneg) { |conneg|
18
+ conneg.set :accept_all_extensions, false
19
+ conneg.set :fallback, :html
20
+ conneg.ignore('/public/')
21
+ conneg.provide([:json, :xml])
22
+ }
23
+
24
+ before do
25
+ content_type negotiated_type
26
+ end
27
+
28
+ get '/hello' do
29
+ response = { :message => 'Hello, World!' }
30
+ respond_to do |wants|
31
+ wants.json { response.to_json }
32
+ wants.xml { response.to_xml }
33
+ wants.other {
34
+ content_type 'text/plain'
35
+ error 406, "Not Acceptable"
36
+ }
37
+ end
38
+ end
@@ -0,0 +1,149 @@
1
+ require 'rack'
2
+ require 'rack/mime'
3
+
4
+ module Rack #:nodoc:#
5
+
6
+ class Conneg
7
+
8
+ VERSION = '0.1.2'
9
+
10
+ def initialize(app)
11
+ @app = app
12
+ @ignores = []
13
+ @opts = {
14
+ :accept_all_extensions => false,
15
+ :fallback => 'text/html'
16
+ }
17
+ @types = []
18
+
19
+ @app.class.module_eval {
20
+ def negotiated_ext ; @rack_conneg_ext ; end #:nodoc:#
21
+ def negotiated_type ; @rack_conneg_type ; end #:nodoc:#
22
+ def respond_to
23
+ wants = { '*/*' => Proc.new { raise TypeError, "No handler for #{@rack_conneg_type}" } }
24
+ def wants.method_missing(ext, *args, &handler)
25
+ type = ext == :other ? '*/*' : Rack::Mime::MIME_TYPES[".#{ext.to_s}"]
26
+ self[type] = handler
27
+ end
28
+
29
+ yield wants
30
+
31
+ (wants[@rack_conneg_type] || wants['*/*']).call
32
+ end
33
+ }
34
+
35
+ if block_given?
36
+ yield self
37
+ end
38
+ end
39
+
40
+ def call(env)
41
+ extension = nil
42
+ path_info = env['PATH_INFO']
43
+ unless @ignores.find { |ignore| ignore.match(path_info) }
44
+ # First, check to see if there's an explicit type requested
45
+ # via the file extension
46
+ mime_type = Rack::Mime.mime_type(::File.extname(path_info),nil)
47
+ if mime_type
48
+ env['PATH_INFO'] = path_info.sub!(/(\..+?)$/,'')
49
+ extension = $1
50
+ if !(accept_all_extensions? || @types.include?(mime_type))
51
+ mime_type = nil
52
+ end
53
+ else
54
+ # Create an array of types out of the HTTP_ACCEPT header, sorted
55
+ # by q value and original order
56
+ accept_types = env['HTTP_ACCEPT'].split(/,/)
57
+ accept_types.each_with_index { |t,i|
58
+ (accept_type,weight) = t.split(/;/)
59
+ weight = weight.nil? ? 1.0 : weight.split(/\=/).last.to_f
60
+ accept_types[i] = { :type => accept_type, :weight => weight, :order => i }
61
+ }
62
+ accept_types.sort! { |a,b|
63
+ ord = b[:weight] <=> a[:weight]
64
+ if ord == 0
65
+ ord = a[:order] <=> b[:order]
66
+ end
67
+ ord
68
+ }
69
+
70
+ # Find the first item in accept_types that matches a registered
71
+ # content type
72
+ accept_types.find { |t|
73
+ re = %r{^#{t[:type].gsub(/\*/,'.+')}$}
74
+ @types.find { |type| re.match(type) ? mime_type = type : nil }
75
+ }
76
+ end
77
+
78
+ mime_type ||= fallback
79
+ @app.instance_variable_set('@rack_conneg_ext',env['rack.conneg.ext'] = extension)
80
+ @app.instance_variable_set('@rack_conneg_type',env['rack.conneg.type'] = mime_type)
81
+ end
82
+ @app.call(env) unless @app.nil?
83
+ end
84
+
85
+ # Should content negotiation accept any file extention passed as part of the URI path,
86
+ # even if it's not one of the registered provided types?
87
+ def accept_all_extensions?
88
+ @opts[:accept_all_extensions] ? true : false
89
+ end
90
+
91
+ # What MIME type should be used as a fallback if negotiation fails? Defaults to 'text/html'
92
+ # since that's what's used to deliver most error message content.
93
+ def fallback
94
+ find_mime_type(@opts[:fallback])
95
+ end
96
+
97
+ # Specifies a route prefix or Regexp that should be ignored by the content negotiator. Use
98
+ # for static files or any other route that should be passed through unaltered.
99
+ def ignore(route)
100
+ route_re = route.kind_of?(Regexp) ? route : %r{^#{route}}
101
+ @ignores << route_re
102
+ end
103
+
104
+ # Register one or more content types that the application offers. Can be a content type string,
105
+ # a file extension, or a symbol (e.g., 'application/xml', '.xml', and :xml are all equivalent).
106
+ def provide(*args)
107
+ args.flatten.each { |type|
108
+ mime_type = find_mime_type(type)
109
+ @types << mime_type
110
+ }
111
+ end
112
+
113
+ # Set a content negotiation option. Valid options are:
114
+ # * :accept_all_extensions - true if all file extensions should be mapped to MIME types whether
115
+ # or not their associated types are specifically provided
116
+ # * :fallback - a content type string, file extention, or symbol representing the MIME type to
117
+ # fall back on if negotiation fails
118
+ def set(key, value)
119
+ opt_key = key.to_sym
120
+ if !@opts.include?(opt_key)
121
+ raise IndexError, "Unknown option: #{key.to_s}"
122
+ end
123
+ @opts[opt_key] = value
124
+ end
125
+
126
+ private
127
+ def find_mime_type(type)
128
+ valid_types = Rack::Mime::MIME_TYPES.values
129
+ mime_type = nil
130
+ if type =~ %r{^[^/]+/[^/]+}
131
+ mime_type = type
132
+ else
133
+ ext = type.to_s
134
+ ext = ".#{ext}" unless ext =~ /^\./
135
+ mime_type = Rack::Mime.mime_type(ext,nil)
136
+ end
137
+ unless valid_types.include?(mime_type)
138
+ raise ValueError, "Unknown MIME type: #{mime_type}"
139
+ end
140
+ return mime_type
141
+ end
142
+ end
143
+
144
+ class Request
145
+ def negotiated_ext ; @env['rack.conneg.ext'] ; end
146
+ def negotiated_type ; @env['rack.conneg.type'] ; end
147
+ end
148
+
149
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-conneg
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Michael B. Klein
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-02-10 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rack
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "1.0"
24
+ version:
25
+ description: Middleware that provides both file extension and HTTP_ACCEPT-type content negotiation for Rack applications
26
+ email: Michael.Klein@oregonstate.edu
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - README.rdoc
33
+ files:
34
+ - README.rdoc
35
+ - lib/rack/conneg.rb
36
+ has_rdoc: true
37
+ homepage:
38
+ licenses: []
39
+
40
+ post_install_message:
41
+ rdoc_options:
42
+ - --main
43
+ - README.rdoc
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ requirements: []
59
+
60
+ rubyforge_project:
61
+ rubygems_version: 1.3.5
62
+ signing_key:
63
+ specification_version: 3
64
+ summary: Content Negotiation middleware for Rack applications
65
+ test_files: []
66
+