Commits

Michael Granger committed 6a0e777

Wire up custom mediatype handlers to content negotiation. Fixes #5.

  • Participants
  • Parent commits cae6b25

Comments (0)

Files changed (4)

File lib/strelka/app/negotiation.rb

 #
 #       plugins :routing, :negotiation
 #
-#       add_content_type :tnetstring, 'text/x-tnetstring' do |response|
-#           tnetstr = nil
-#           begin
-#               response.body.rewind
-#               tnetstr = TNetString.dump( response.body )
-#           rescue => err
-#               self.log.error "%p while transforming entity body to a TNetString: %s" %
-#                   [ err.class, err.message ]
-#               return false
-#           else
-#               response.body = tnetstr
-#               response.content_type = 'text/x-tnetstring'
-#               return true
-#           end
-#       end
+#       add_content_type :tnetstring, 'text/x-tnetstring', TNetstring.method( :dump )
 #
 #   end # class UserService
 #
 	# Class methods to add to classes with content-negotiation.
 	module ClassMethods # :nodoc:
 
-		# Content-type tranform registry, keyed by name
-		@content_type_transforms = {}
-		attr_reader :content_type_transforms
-
-		# Content-type transform names, keyed by mimetype
-		@transform_names = {}
-		attr_reader :transform_names
-
-
-		### Extension callback -- add instance variables to extending objects.
-		def inherited( subclass )
-			super
-			subclass.instance_variable_set( :@content_type_transforms, @content_type_transforms.dup )
-			subclass.instance_variable_set( :@transform_names, @transform_names.dup )
-		end
-
-
 		### Define a new media-type associated with the specified +name+ and +mimetype+. Responses
-		### whose requests accept content of the given +mimetype+ will pass their response to the
-		### specified +transform_block+, which should re-write the response's entity body if it can
-		### transform it to its mimetype. If it successfully does so, it should return +true+, else
-		### the next-best mimetype's transform will be called, etc.
-		def add_content_type( name, mimetype, &transform_block )
-			self.transform_names[ mimetype ] = name
-			self.content_type_transforms[ name ] = transform_block
+		### whose requests accept content of the given +mimetype+ will #call the
+		### specified +callback+ with the current response body, and should return the
+		### transformed body. If the conversion failed, the callback can either raise an
+		### exception or return +nil+, either of which will continue on to any remaining
+		### transforms. If no +callback+ is given, the method's block will be used.
+		def add_content_type( name, mimetype, callback=nil ) # :yield: response.body
+			callback ||= Proc.new # Use the block
+			Strelka::HTTPResponse::Negotiation.mimetype_map[ name.to_sym ] = mimetype
+			Strelka::HTTPResponse::Negotiation.stringifiers[ mimetype ] = callback
 		end
 
 	end # module ClassMethods

File lib/strelka/httpresponse/negotiation.rb

 
 require 'strelka/constants'
 require 'strelka/exceptions'
+require 'strelka/mixins'
 require 'strelka/httpresponse' unless defined?( Strelka::HTTPResponse )
 
 
 # is acceptable according to its request's `Accept*` headers.
 #
 module Strelka::HTTPResponse::Negotiation
+	extend Strelka::MethodUtilities
 	include Strelka::Constants
 
 	# TODO: Perhaps replace this with something like this:
 	#   Mongrel2::Config::Mimetype.to_hash( :extension => :mimetype )
-	BUILTIN_MIMETYPES = {
+	BUILTIN_MIMETYPE_MAP = {
 		:html => 'text/html',
 		:text => 'text/plain',
 
 		:atom => 'application/atom+xml',
 	}
 
-	# A collection of stringifier callbacks, keyed by mimetype. If an object other
-	# than a String is returned by a content callback, and an entry for the callback's
-	# mimetype exists in this Hash, it will be #call()ed to stringify the object.
-	STRINGIFIERS = {
+	# A collection of default stringifier callbacks, keyed by mimetype. If an entry
+	# for the content-negotiation callback's mimetype exists in this Hash, it will
+	# be #call()ed on the callback's return value to stringify the body.
+	BUILTIN_STRINGIFIERS = {
 		'application/x-yaml' => YAML.method( :dump ),
 		'application/json'   => Yajl.method( :dump ),
 		'text/plain'         => Proc.new {|obj| obj.to_s },
 	]
 
 
+	##
+	# The Hash of symbolic mediatypes of the form:
+	#   { <name (Symbol)> => <mimetype> }
+	singleton_attr_reader :mimetype_map
+	@mimetype_map = BUILTIN_MIMETYPE_MAP.dup
+
+	##
+	# The Hash of stringification callbacks, keyed by mimetype.
+	singleton_attr_reader :stringifiers
+	@stringifiers = BUILTIN_STRINGIFIERS.dup
+
+
 	### Add some instance variables for negotiation.
 	def initialize( * )
 		@mediatype_callbacks = {}
 	### to HTTP::OK.
 	def for( *mediatypes, &callback )
 		mediatypes.each do |mimetype|
-			mimetype = BUILTIN_MIMETYPES[ mimetype ] if mimetype.is_a?( Symbol )
+			if mimetype.is_a?( Symbol )
+				mimetype = Strelka::HTTPResponse::Negotiation.mimetype_map[ mimetype ] or
+					raise "No known mimetype mapped to %p" % [ mimetype ]
+			end
+
 			self.mediatype_callbacks[ mimetype ] = callback
 		end
 
 	### until one returns a true-ish value, which becomes the new entity
 	### body. If the body object is not a String,
 	def transform_content_type
+		self.log.debug "Applying content-type transforms (if any)"
 		return if self.mediatype_callbacks.empty?
 
-		self.log.debug "Applying content-type transforms (if any)"
+		self.log.debug "  transform callbacks registered: %p" % [ self.mediatype_callbacks ]
 		self.better_mediatypes.each do |mediatype|
 			callbacks = self.mediatype_callbacks.find_all do |mimetype, _|
 				mediatype =~ mimetype
 			end
 
 			if callbacks.empty?
-				self.log.debug "  no transforms for %s" % [ mediatype ]
+				self.log.debug "    no transforms for %s" % [ mediatype ]
 			else
+				self.log.debug "    %d transform/s for %s" % [ callbacks.length, mediatype ]
 				callbacks.each do |mimetype, callback|
 					return if self.try_content_type_callback( mimetype, callback )
 				end
 
 		new_body = callback.call( mimetype ) or return false
 
-		self.log.debug "  successfully transformed: %p! Setting up response." % [ new_body ]
-		new_body = STRINGIFIERS[ mimetype ].call( new_body ) if
-			STRINGIFIERS.key?( mimetype ) && !new_body.is_a?( String )
+		self.log.debug "  successfully transformed: %p! Setting up response." % [ new_body.class ]
+		stringifiers = Strelka::HTTPResponse::Negotiation.stringifiers
+		if stringifiers.key?( mimetype )
+			new_body = stringifiers[ mimetype ].call( new_body )
+		else
+			self.log.debug "    no stringifier registered for %p" % [ mimetype ]
+		end
 
 		self.body = new_body
 		self.content_type = mimetype.dup # :TODO: Why is this frozen?
 
 				# Don't close the FD when this IO goes out of scope
 				oldbody = self.body
-				oldbody.auto_close = false
+				oldbody.autoclose = false
 
 				# Re-open the same file descriptor, but transcoding to the wanted encoding
 				self.body = IO.for_fd( oldbody.fileno, internal_encoding: enc )

File spec/strelka/app/negotiation_spec.rb

 				def initialize( appid='conneg-test', sspec=TEST_SEND_SPEC, rspec=TEST_RECV_SPEC )
 					super
 				end
+
+				add_content_type :tnetstring, 'text/x-tnetstring', TNetstring.method(:dump)
+
+				def handle_request( req )
+					super do
+						res = req.response
+						res.for( :tnetstring ) {[ 'an', {'array' => 'of stuff'} ]}
+						res.for( :html ) do
+							"<html><head><title>Yep</title></head><body>Yeah!</body></html>"
+						end
+						res
+					end
+				end
 			end
 		end
 
 		end
 
 
-		it "has its config inherited by subclasses" do
-			@app.add_content_type :text, 'text/plain' do
-				"It's all text now, baby"
-			end
-			subclass = Class.new( @app )
-
-			subclass.transform_names.should == @app.transform_names
-			subclass.transform_names.should_not equal( @app.transform_names )
-			subclass.content_type_transforms.should == @app.content_type_transforms
-			subclass.content_type_transforms.should_not equal( @app.content_type_transforms )
-		end
-
 		it "gets requests that have been extended with content-negotiation" do
 			req = @request_factory.get( '/service/user/estark' )
 			@app.new.handle( req )
 				should include( Strelka::HTTPResponse::Negotiation )
 		end
 
+		it "adds custom content-type transforms to outgoing responses" do
+			req = @request_factory.get( '/service/user/astark', :accept => 'text/x-tnetstring' )
+			res = @app.new.handle( req )
+			res.content_type.should == 'text/x-tnetstring'
+			res.body.read.should == '28:2:an,19:5:array,8:of stuff,}]'
+		end
+
 	end
 
 

File spec/strelka/httpresponse/negotiation_spec.rb

 		it "can provide blocks for bodies of several different mediatypes" do
 			@req.headers.accept = 'application/x-yaml, application/json; q=0.7, text/xml; q=0.2'
 
-			@res.for( 'application/json' ) { %{["a JSON dump"]} }
-			@res.for( 'application/x-yaml' ) { "---\na: YAML dump\n\n" }
+			@res.for( 'application/json' ) { ["a JSON dump"] }
+			@res.for( 'application/x-yaml' ) { { 'a' => "YAML dump" } }
 
 			@res.negotiated_body.rewind
-			@res.negotiated_body.read.should == "---\na: YAML dump\n\n"
+			@res.negotiated_body.read.should == "---\na: YAML dump\n"
 			@res.content_type.should == "application/x-yaml"
 			@res.header_data.should =~ /accept(?!-)/i
 		end
 			@res.header_data.should =~ /accept(?!-)/i
 		end
 
+		it "raises an exception if given a block for an unknown symbolic mediatype" do
+			expect {
+				@res.for( :yaquil ) {}
+			}.to raise_error( StandardError, /no known mimetype/i )
+		end
+
 	end
 
 
 				@res.body = File.open( __FILE__, 'r:iso-8859-5' )
 				@res.content_type = 'text/plain'
 
-				@res.negotiated_body.encoding.should == Encoding::KOI8_R
+				@res.negotiated_body.read.encoding.should == Encoding::KOI8_R
 				@res.header_data.should =~ /accept-charset(?!-)/i
 			end
 		end