Commits

Michael Granger committed 10a32ad

Add GET /<uuid>/metadata, GET /<uuid>/metadata/<key>, POST /<uuid>/metadata

  • Participants
  • Parent commits 12f781e

Comments (0)

Files changed (6)

 PUT /v1/«uuid» *
 
 # remove an asset and its metadata
-DELETE /v1/«uuid»
+DELETE /v1/«uuid» *
 
 # retrieve all metadata associated with an asset
-GET /v1/«uuid»/metadata
+GET /v1/«uuid»/metadata *
 
 # retrieve values for an asset's metadata key
-GET /v1/«uuid»/metadata/«key»
+GET /v1/«uuid»/metadata/«key» *
 
 # append additional metadata for an asset
 POST /v1/«uuid»/metadata
 	}
 
 	require 'thingfish/mixins'
+	require 'thingfish/datastore'
+	require 'thingfish/metastore'
 	extend MethodUtilities
 
 
 	attr_reader :metastore
 
 
+	########################################################################
+	### P L U G I N S
+	########################################################################
+
 	#
 	# Global parmas
 	#
 	plugin :parameters
 	param :uuid
+	param :key, :word
+
+
+	#
+	# Content negotiation
+	#
+	plugin :negotiation
 
 
 	#
 	plugin :routing
 	router :exclusive
 
+
+	#
+	# Datastore routes
+	#
+
 	# GET /«uuid»
 	# Fetch an object by ID
 	get ':uuid' do |req|
 
 		res = req.response
 		res.body = object
-		res.content_type = metadata[:format]
+		res.content_type = metadata['format']
 
 		return res
 	end
 		res.headers.location = url
 		res.status = HTTP::CREATED
 
+		res.for( :text, :json, :yaml ) { metadata }
+
 		return res
 	end
 
 	delete ':uuid' do |req|
 		uuid = req.params[:uuid]
 
-		self.datastore.remove( uuid )
-		self.metastore.remove( uuid )
+		self.datastore.remove( uuid ) or finish_with( HTTP::NOT_FOUND, "No such object." )
+		metadata = self.metastore.remove( uuid )
 
 		res = req.response
 		res.status = HTTP::OK
 
+		# TODO: Remove in favor of default metadata when the metastore
+		# knows what that is.
+		res.for( :text ) do
+			"%d bytes for %s deleted." % [ metadata['extent'], uuid ]
+		end
+		res.for( :json, :yaml ) {{ uuid: uuid, extent: metadata['extent'] }}
+
 		return res
 	end
 
 
-	require 'thingfish/datastore'
-	require 'thingfish/metastore'
+	#
+	# Metastore routes
+	#
+
+	# GET /«uuid»/metadata
+	# Fetch all metadata for «uuid».
+	get ':uuid/metadata' do |req|
+		uuid = req.params[:uuid]
+
+		finish_with( HTTP::NOT_FOUND, "No such object." ) unless self.metastore.include?( uuid )
+
+		res = req.response
+		res.status = HTTP::OK
+		res.for( :json, :yaml ) { self.metastore.fetch( uuid ) }
+
+		return res
+	end
+
+
+	# GET /«uuid»/metadata/«key»
+	# Fetch metadata value associated with «key» for «uuid».
+	get ':uuid/metadata/:key' do |req|
+		uuid = req.params[:uuid]
+		key  = req.params[:key]
+
+		finish_with( HTTP::NOT_FOUND, "No such object." ) unless self.metastore.include?( uuid )
+
+		res = req.response
+		res.status = HTTP::OK
+		res.for( :json, :yaml ) { self.metastore.fetch( uuid, key ) }
+
+		return res
+	end
+
+
+	# POST /«uuid»/metadata
+	# Merge new metadata into the existing metadata for «uuid».
+	post ':uuid/metadata' do |req|
+		uuid = req.params[:uuid]
+
+		finish_with( HTTP::NOT_FOUND, "No such object." ) unless self.metastore.include?( uuid )
+
+		new_metadata = req.params.fields.dup
+		new_metadata.delete( :uuid )
+
+		self.metastore.merge( uuid, new_metadata )
+
+		res = req.response
+		res.status = HTTP::NO_CONTENT
+
+		return res
+	end
 
 
 	#########
 	### Return a Hash of default metadata extracted from the given +request+.
 	def extract_default_metadata( request )
 		return {
-			:useragent     => request.headers.user_agent,
-			:extent        => request.headers.content_length,
-			:uploadaddress => request.remote_ip,
-			:format        => request.content_type
+			'useragent'     => request.headers.user_agent,
+			'extent'        => request.headers.content_length,
+			'uploadaddress' => request.remote_ip,
+			'format'        => request.content_type
 		}
 	end
 

lib/thingfish/metastore/memory.rb

 		if keys.empty?
 			self.log.debug "Fetching metadata for OID %s" % [ oid ]
 			return @storage[ oid ]
+		elsif keys.length == 1
+			data = @storage[ oid ] or return nil
+			return data[ keys.first ]
 		else
 			self.log.debug "Fetching metadata for %p for OID %s" % [ keys, oid ]
-			data = @storage[ oid ] or return []
+			data = @storage[ oid ] or return nil
 			return data.values_at( *keys )
 		end
 	end
 
 BEGIN {
 	require 'pathname'
-	basedir = Pathname.new( __FILE__ ).dirname.parent.parent
 
-	srcdir = basedir.parent
-	libdir = basedir + "lib"
+	basedir = Pathname.new( __FILE__ ).dirname.parent
+	strelkadir = basedir.parent + 'Strelka'
+	strelkalibdir = strelkadir + 'lib'
 
-	$LOAD_PATH.unshift( basedir.to_s ) unless $LOAD_PATH.include?( basedir.to_s )
-	$LOAD_PATH.unshift( libdir.to_s ) unless $LOAD_PATH.include?( libdir.to_s )
+	$LOAD_PATH.unshift( strelkalibdir.to_s ) unless $LOAD_PATH.include?( strelkalibdir.to_s )
 }
 
 # SimpleCov test coverage reporting; enable this using the :coverage rake task

spec/thingfish/metastore/memory_spec.rb

 	end
 
 
+	it "returns nil when fetching metadata for an object that doesn't exist" do
+		expect( @store.fetch(TEST_UUID) ).to be_nil
+	end
+
+
 	it "doesn't care about the case of the UUID when saving and fetching data" do
 		@store.save( TEST_UUID.downcase, TEST_METADATA )
 		expect( @store.fetch(TEST_UUID) ).to eq( TEST_METADATA )
 	end
 
 
+	it "can fetch a single metadata value for a given oid" do
+		@store.save( TEST_UUID, TEST_METADATA )
+		expect( @store.fetch(TEST_UUID, :format) ).to eq( TEST_METADATA[:format] )
+		expect( @store.fetch(TEST_UUID, :extent) ).to eq( TEST_METADATA[:extent] )
+	end
+
+
 	it "can fetch a slice of data for a given oid" do
 		@store.save( TEST_UUID, TEST_METADATA )
-		expect( @store.fetch(TEST_UUID, :format) ).to eq( [TEST_METADATA[:format]] )
-		expect( @store.fetch(TEST_UUID, :extent) ).to eq( [TEST_METADATA[:extent]] )
+		expect( @store.fetch(TEST_UUID, :format, :extent) )
+			.to eq( TEST_METADATA.values_at(:format, :extent) )
+	end
+
+
+	it "returns nil when fetching a slice of data for an object that doesn't exist" do
+		expect( @store.fetch(TEST_UUID, :format) ).to be_nil
 	end
 
 
 	it "doesn't care about the case of the UUID when fetching data" do
 		@store.save( TEST_UUID, TEST_METADATA )
-		expect( @store.fetch(TEST_UUID.downcase, :format) ).to eq( [TEST_METADATA[:format]] )
+		expect( @store.fetch(TEST_UUID.downcase, :format) ).to eq( TEST_METADATA[:format] )
 	end
 
 
 		@store.save( TEST_UUID, TEST_METADATA )
 		@store.merge( TEST_UUID, format: 'image/jpeg' )
 
-		expect( @store.fetch(TEST_UUID, :format) ).to eq( ['image/jpeg'] )
+		expect( @store.fetch(TEST_UUID, :format) ).to eq( 'image/jpeg' )
 	end
 
 	it "doesn't care about the case of the UUID when updating data" do
 		@store.save( TEST_UUID, TEST_METADATA )
 		@store.merge( TEST_UUID.downcase, format: 'image/jpeg' )
 
-		expect( @store.fetch(TEST_UUID, :format) ).to eq( ['image/jpeg'] )
+		expect( @store.fetch(TEST_UUID, :format) ).to eq( 'image/jpeg' )
 	end
 
 	it "can remove metadata for a UUID" do

spec/thingfish_spec.rb

 		@text_io = StringIO.new( TEST_TEXT_DATA.dup )
 	end
 
-	let( :handler )      { described_class.new(TEST_APPID, TEST_SEND_SPEC, TEST_RECV_SPEC) }
-	let( :factory )      { Mongrel2::RequestFactory.new(:route => '/') }
+	let( :handler ) { described_class.new(TEST_APPID, TEST_SEND_SPEC, TEST_RECV_SPEC) }
 
 
 	#
 	end
 
 
-	it 'accepts a POSTed upload' do
-		req = factory.post( '/', TEST_TEXT_DATA, content_type: 'text/plain' )
-		res = handler.handle( req )
+	context "datastore api" do
 
-		expect( res.status_line ).to match( /201 created/i )
-		expect( res.headers.location.to_s ).to match( %r:/#{UUID_PATTERN}$: )
+		let( :factory ) {
+			Mongrel2::RequestFactory.new(
+				:route => '/',
+				:headers => {:accept => '*/*'})
+		}
+
+		it 'accepts a POSTed upload' do
+			req = factory.post( '/', TEST_TEXT_DATA, content_type: 'text/plain' )
+			res = handler.handle( req )
+
+			expect( res.status_line ).to match( /201 created/i )
+			expect( res.headers.location.to_s ).to match( %r:/#{UUID_PATTERN}$: )
+		end
+
+
+		it 'replaces content via PUT' do
+			uuid = handler.datastore.save( @text_io )
+			handler.metastore.save( uuid, {'format' => 'text/plain'} )
+
+			req = factory.put( "/#{uuid}", @png_io, content_type: 'image/png' )
+			res = handler.handle( req )
+
+			expect( res.status ).to eq( HTTP::NO_CONTENT )
+			expect( handler.datastore.fetch(uuid).read ).to eq( TEST_PNG_DATA )
+			expect( handler.metastore.fetch(uuid) ).to include( 'format' => 'image/png' )
+		end
+
+
+		it "doesn't case about the case of the UUID when replacing content via PUT" do
+			uuid = handler.datastore.save( @text_io )
+			handler.metastore.save( uuid, {'format' => 'text/plain'} )
+
+			req = factory.put( "/#{uuid.upcase}", @png_io, content_type: 'image/png' )
+			res = handler.handle( req )
+
+			expect( res.status ).to eq( HTTP::NO_CONTENT )
+			expect( handler.datastore.fetch(uuid).read ).to eq( TEST_PNG_DATA )
+			expect( handler.metastore.fetch(uuid) ).to include( 'format' => 'image/png' )
+		end
+
+
+		it "can fetch an uploaded chunk of data" do
+			uuid = handler.datastore.save( @png_io )
+			handler.metastore.save( uuid, {'format' => 'image/png'} )
+
+			req = factory.get( "/#{uuid}" )
+			result = handler.handle( req )
+
+			expect( result.status_line ).to match( /200 ok/i )
+			expect( result.body.read ).to eq( @png_io.string )
+			expect( result.headers.content_type ).to eq( 'image/png' )
+		end
+
+
+		it "doesn't care about the case of the UUID when fetching uploaded data" do
+			uuid = handler.datastore.save( @png_io )
+			handler.metastore.save( uuid, {'format' => 'image/png'} )
+
+			req = factory.get( "/#{uuid.upcase}" )
+			result = handler.handle( req )
+
+			expect( result.status_line ).to match( /200 ok/i )
+			expect( result.body.read ).to eq( @png_io.string )
+			expect( result.headers.content_type ).to eq( 'image/png' )
+		end
+
+
+		it "can remove everything associated with an object id" do
+			uuid = handler.datastore.save( @png_io )
+			handler.metastore.save( uuid, {
+				'format' => 'image/png',
+				'extent' => 288,
+			})
+
+			req = factory.delete( "/#{uuid}" )
+			result = handler.handle( req )
+
+			expect( result.status_line ).to match( /200 ok/i )
+			expect( handler.metastore ).to_not include( uuid )
+			expect( handler.datastore ).to_not include( uuid )
+		end
+
+
+		it "returns a 404 Not Found when asked to remove an object that doesn't exist" do
+			req = factory.delete( "/#{TEST_UUID}" )
+			result = handler.handle( req )
+
+			expect( result.status_line ).to match( /404 not found/i )
+		end
+
 	end
 
 
-	it 'replaces content via PUT' do
-		uuid = handler.datastore.save( @text_io )
-		handler.metastore.save( uuid, {format: 'text/plain'} )
+	context "metastore api" do
 
-		req = factory.put( "/#{uuid}", @png_io, content_type: 'image/png' )
-		res = handler.handle( req )
+		let( :factory ) {
+			Mongrel2::RequestFactory.new(
+				:route => '/',
+				:headers => {:accept => 'application/json'})
+		}
 
-		expect( res.status ).to eq( HTTP::NO_CONTENT )
-		expect( handler.datastore.fetch(uuid).read ).to eq( TEST_PNG_DATA )
-		expect( handler.metastore.fetch(uuid) ).to include( format: 'image/png' )
+		it "can fetch the metadata associated with uploaded data" do
+			uuid = handler.datastore.save( @png_io )
+			handler.metastore.save( uuid, {
+				'format' => 'image/png',
+				'extent' => 288,
+			})
+
+			req = factory.get( "/#{uuid}/metadata" )
+			result = handler.handle( req )
+			content = result.body.read
+
+			content_hash = Yajl::Parser.parse( content )
+
+			expect( result.status ).to eq( 200 )
+			expect( result.headers.content_type ).to eq( 'application/json' )
+			expect( content_hash ).to be_a( Hash )
+			expect( content_hash['extent'] ).to eq( 288 )
+		end
+
+
+		it "returns a 404 Not Found when fetching metadata for an object that doesn't exist" do
+			req = factory.get( "/#{TEST_UUID}/metadata" )
+			result = handler.handle( req )
+
+			expect( result.status_line ).to match( /404 not found/i )
+		end
+
+
+		it "can fetch a value for a specific metadata key" do
+			uuid = handler.datastore.save( @png_io )
+			handler.metastore.save( uuid, {
+				'format' => 'image/png',
+				'extent' => 288,
+			})
+
+			req = factory.get( "/#{uuid}/metadata/extent" )
+			result = handler.handle( req )
+			content = Yajl::Parser.parse( result.body.read )
+
+			expect( result.status ).to eq( 200 )
+			expect( result.headers.content_type ).to eq( 'application/json' )
+			expect( content ).to be( 288 )
+		end
+
+
+		it "returns a 404 Not Found when fetching a specific metadata value for a uuid that doesn't exist" do
+			req = factory.get( "/#{TEST_UUID}/metadata/extent" )
+			result = handler.handle( req )
+
+			expect( result.status_line ).to match( /404 not found/i )
+		end
+
+
+		it "doesn't error when fetching a nonexistent metadata value" do
+			uuid = handler.datastore.save( @png_io )
+			handler.metastore.save( uuid, {
+				'format' => 'image/png',
+				'extent' => 288,
+			})
+
+			req = factory.get( "/#{uuid}/metadata/hururrgghh" )
+			result = handler.handle( req )
+
+			content = Yajl::Parser.parse( result.body.read )
+
+			expect( result.status ).to eq( 200 )
+			expect( result.headers.content_type ).to eq( 'application/json' )
+
+			expect( content ).to be_nil
+		end
+
+
+		it "can merge in new metadata for an existing resource" do
+			uuid = handler.datastore.save( @png_io )
+			handler.metastore.save( uuid, {
+				'format' => 'image/png',
+				'extent' => 288,
+			})
+
+			body_json = Yajl.dump({ 'comment' => 'Ignore me!' })
+			req = factory.post( "/#{uuid}/metadata", body_json, 'Content-type' => 'application/json' )
+			result = handler.handle( req )
+
+			expect( result.status ).to eq( HTTP::NO_CONTENT )
+			expect( handler.metastore.fetch(uuid, 'comment') ).to eq( 'Ignore me!' )
+		end
+
 	end
-
-
-	it "doesn't case about the case of the UUID when replacing content via PUT" do
-		uuid = handler.datastore.save( @text_io )
-		handler.metastore.save( uuid, {format: 'text/plain'} )
-
-		req = factory.put( "/#{uuid.upcase}", @png_io, content_type: 'image/png' )
-		res = handler.handle( req )
-
-		expect( res.status ).to eq( HTTP::NO_CONTENT )
-		expect( handler.datastore.fetch(uuid).read ).to eq( TEST_PNG_DATA )
-		expect( handler.metastore.fetch(uuid) ).to include( format: 'image/png' )
-	end
-
-
-	it "can fetch an uploaded chunk of data" do
-		uuid = handler.datastore.save( @png_io )
-		handler.metastore.save( uuid, {format: 'image/png'} )
-
-		req = factory.get( "/#{uuid}" )
-		result = handler.handle( req )
-
-		expect( result.status_line ).to match( /200 ok/i )
-		expect( result.body.read ).to eq( @png_io.string )
-		expect( result.headers.content_type ).to eq( 'image/png' )
-	end
-
-
-	it "doesn't care about the case of the UUID when fetching uploaded data" do
-		uuid = handler.datastore.save( @png_io )
-		handler.metastore.save( uuid, {format: 'image/png'} )
-
-		req = factory.get( "/#{uuid.upcase}" )
-		result = handler.handle( req )
-
-		expect( result.status_line ).to match( /200 ok/i )
-		expect( result.body.read ).to eq( @png_io.string )
-		expect( result.headers.content_type ).to eq( 'image/png' )
-	end
-
-
-	it "can remove everything associated with an object id" do
-		uuid = handler.datastore.save( @png_io )
-		handler.metastore.save( uuid, {format: 'image/png'} )
-
-		req = factory.delete( "/#{uuid}" )
-		result = handler.handle( req )
-
-		expect( result.status_line ).to match( /200 ok/i )
-		expect( handler.metastore ).to_not include( uuid )
-		expect( handler.datastore ).to_not include( uuid )
-	end
-
-
-	it "doesn't error when asked to remove an object that doesn't exist" do
-		req = factory.delete( "/#{TEST_UUID}" )
-		result = handler.handle( req )
-
-		expect( result.status_line ).to match( /200 ok/i )
-	end
-
 end
 
 # vim: set nosta noet ts=4 sw=4 ft=rspec: