rack-conneg 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+