Commits

Michael Granger committed a678e38

Implement a few more metadata API methods.

- POST /<uuid>/metadata/<key>
- PUT /<uuid>/metadata/<key>
- Make error messages more consistent.
- Split out testing constants into a seperate file.

  • Participants
  • Parent commits 3d54bb8

Comments (0)

Files changed (8)

 lib/thingfish/metastore.rb
 lib/thingfish/metastore/memory.rb
 lib/thingfish/mixins.rb
+spec/constants.rb
 spec/helpers.rb
 spec/spec.opts
 spec/thingfish/datastore/memory_spec.rb
 POST /v1/«uuid»/metadata *
 
 # add a value for an asset's specific metadata key
-POST /v1/«uuid»/metadata/«key»
+POST /v1/«uuid»/metadata/«key» *
 
 # replace metadata for an asset
 PUT /v1/«uuid»/metadata
 
 # update an asset's specific metadata key
-PUT /v1/«uuid»/metadata/«key»
+PUT /v1/«uuid»/metadata/«key» *
 
 # remove all user metadata for an asset
 DELETE /v1/«uuid»/metadata
 	get ':uuid' do |req|
 		uuid = req.params[:uuid]
 		object = self.datastore.fetch( uuid ) or
-			finish_with HTTP::NOT_FOUND, "no such object"
+			finish_with HTTP::NOT_FOUND, "No such object."
 		metadata = self.metastore.fetch( uuid )
 
 		res = req.response
 
 		uuid = req.params[:uuid]
 		object = self.datastore.fetch( uuid ) or
-			finish_with HTTP::NOT_FOUND, "no such object"
+			finish_with HTTP::NOT_FOUND, "No such object."
 
 		self.datastore.replace( uuid, req.body )
 		self.metastore.merge( uuid, metadata )
 	end
 
 
+	# POST /«uuid»/metadata/«key»
+	# Create a metadata value associated with «key» for «uuid».
+	post ':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 )
+		finish_with( HTTP::CONFLICT, "Key already exists." ) unless
+			self.metastore.fetch( uuid, key ).nil?
+
+		self.metastore.save( uuid, key, req.body.read )
+
+		res = req.response
+		res.headers.location = req.uri.to_s
+		res.body = nil
+		res.status = HTTP::CREATED
+
+		return res
+	end
+
+
+	# PUT /«uuid»/metadata/«key»
+	# Replace or create a metadata value associated with «key» for «uuid».
+	put ':uuid/metadata/:key' do |req|
+		uuid = req.params[:uuid]
+		key  = req.params[:key]
+
+		finish_with( HTTP::FORBIDDEN, "Protected metadata." ) if PROTECTED_METADATA_KEYS.include?( key )
+		finish_with( HTTP::NOT_FOUND, "No such object." ) unless self.metastore.include?( uuid )
+		previous_value = self.metastore.fetch( uuid, key )
+
+		self.metastore.save( uuid, key, req.body.read )
+
+		res = req.response
+		res.body = nil
+
+		if previous_value
+			res.status = HTTP::NO_CONTENT
+		else
+			res.headers.location = req.uri.to_s
+			res.status = HTTP::CREATED
+		end
+
+		return res
+	end
+
+
 	# POST /«uuid»/metadata
 	# Merge new metadata into the existing metadata for «uuid».
 	post ':uuid/metadata' do |req|

lib/thingfish/metastore/memory.rb

 	end
 
 
-	### Save the +metadata+ for the specified +oid+.
-	def save( oid, metadata )
+	### Save the metadata for the specified +oid+.
+	### +args+ can ether be a Hash of metadata pairs, or a single key and value.
+	###
+	###     save( oid, {} )
+	###     save( oid, key, value )
+	###
+	def save( oid, *args )
 		oid = self.normalize_oid( oid )
-		self.log.debug "Saving %d metadata attributes for OID %s: %s" %
-			[ metadata.length, oid, metadata.keys.join(',') ]
-		@storage[ oid ] = metadata.dup
+
+		metadata = args.shift
+
+		if metadata.is_a?( Hash )
+			@storage[ oid ] = metadata.dup
+		else
+			@storage[ oid ] ||= {}
+			@storage[ oid ][ metadata.to_s ] = args.shift
+		end
 	end
 
 

spec/constants.rb

 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "application/rtf",
 		 "created"       => Time.parse('2010-10-14 00:08:21 UTC'),
-		 "title"         => "spec/data/files/How to use the Public folder.rtf"},
+		 "title"         => "How to use the Public folder.rtf"},
 		{"useragent"     => "ChunkersTheClown v2.0",
 		 "extent"        => 832604,
 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "image/jpeg",
 		 "created"       => Time.parse('2011-09-06 20:10:54 UTC'),
-		 "title"         => "spec/data/files/IMG_0316.JPG"},
+		 "title"         => "IMG_0316.JPG"},
 		{"useragent"     => "ChunkersTheClown v2.0",
 		 "extent"        => 2253642,
 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "image/jpeg",
 		 "created"       => Time.parse('2011-09-06 20:10:49 UTC'),
-		 "title"         => "spec/data/files/IMG_0544.JPG"},
+		 "title"         => "IMG_0544.JPG"},
 		{"useragent"     => "ChunkersTheClown v2.0",
 		 "extent"        => 694785,
 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "image/jpeg",
 		 "created"       => Time.parse('2011-09-06 20:10:52 UTC'),
-		 "title"         => "spec/data/files/IMG_0552.JPG"},
+		 "title"         => "IMG_0552.JPG"},
 		{"useragent"     => "ChunkersTheClown v2.0",
 		 "extent"        => 1579773,
 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "image/jpeg",
 		 "created"       => Time.parse('2011-09-06 20:10:56 UTC'),
-		 "title"         => "spec/data/files/IMG_0748.JPG"},
+		 "title"         => "IMG_0748.JPG"},
 		{"useragent"     => "ChunkersTheClown v2.0",
 		 "extent"        => 6464493,
 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "image/jpeg",
 		 "created"       => Time.parse('2011-10-14 05:05:23 UTC'),
-		 "title"         => "spec/data/files/IMG_1700.JPG"},
+		 "title"         => "IMG_1700.JPG"},
 		{"useragent"     => "ChunkersTheClown v2.0",
 		 "extent"        => 388727,
 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "image/jpeg",
 		 "created"       => Time.parse('2011-12-28 01:23:27 UTC'),
-		 "title"         => "spec/data/files/IMG_3553.jpg"},
+		 "title"         => "IMG_3553.jpg"},
 		{"useragent"     => "ChunkersTheClown v2.0",
 		 "extent"        => 1354,
 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "text/plain",
 		 "created"       => Time.parse('2013-09-09 15:43:31 UTC'),
-		 "title"         => "spec/data/files/agilemanifesto.txt"},
+		 "title"         => "agilemanifesto.txt"},
 		{"useragent"     => "ChunkersTheClown v2.0",
 		 "extent"        => 3059035,
 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "image/jpeg",
 		 "created"       => Time.parse('2013-04-18 00:25:56 UTC'),
-		 "title"         => "spec/data/files/bacon.jpg"},
+		 "title"         => "bacon.jpg"},
 		{"useragent"     => "ChunkersTheClown v2.0",
 		 "extent"        => 71860,
 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "image/jpeg",
 		 "created"       => Time.parse('2011-09-06 20:10:57 UTC'),
-		 "title"         => "spec/data/files/boom.jpg"},
+		 "title"         => "boom.jpg"},
 		{"useragent"     => "ChunkersTheClown v2.0",
 		 "extent"        => 2115410,
 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "audio/mp3",
 		 "created"       => Time.parse('2013-09-09 15:42:49 UTC'),
-		 "title"         => "spec/data/files/craigslist_erotica.mp3"},
+		 "title"         => "craigslist_erotica.mp3"},
 		{"useragent"     => "ChunkersTheClown v2.0",
 		 "extent"        => 377445,
 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "image/jpeg",
 		 "created"       => Time.parse('2012-02-09 17:06:44 UTC'),
-		 "title"         => "spec/data/files/cubes.jpg"},
+		 "title"         => "cubes.jpg"},
 		{"useragent"     => "ChunkersTheClown v2.0",
 		 "extent"        => 240960,
 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "audio/mp3",
 		 "created"       => Time.parse('2013-09-09 15:42:58 UTC'),
-		 "title"         => "spec/data/files/gay_clowns.mp3"},
+		 "title"         => "gay_clowns.mp3"},
 		{"useragent"     => "ChunkersTheClown v2.0",
 		 "extent"        => 561792,
 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "image/jpeg",
 		 "created"       => Time.parse('2011-09-06 20:10:57 UTC'),
-		 "title"         => "spec/data/files/grass2.jpg"},
+		 "title"         => "grass2.jpg"},
 		{"useragent"     => "ChunkersTheClown v2.0",
 		 "extent"        => 1104950,
 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "image/jpeg",
 		 "created"       => Time.parse('2013-09-09 15:37:25 UTC'),
-		 "title"         => "spec/data/files/joss.jpg"},
+		 "title"         => "joss.jpg"},
 		{"useragent"     => "ChunkersTheClown v2.0",
 		 "extent"        => 163,
 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "text/plain",
 		 "created"       => Time.parse('2013-01-23 07:52:44 UTC'),
-		 "title"         => "spec/data/files/macbook.txt"},
+		 "title"         => "macbook.txt"},
 		{"useragent"     => "ChunkersTheClown v2.0",
 		 "extent"        => 2130567,
 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "image/png",
 		 "created"       => Time.parse('2012-03-15 05:15:07 UTC'),
-		 "title"         => "spec/data/files/marbles.png"},
+		 "title"         => "marbles.png"},
 		{"useragent"     => "ChunkersTheClown v2.0",
 		 "extent"        => 8971,
 		 "uploadaddress" => "127.0.0.1",
 		 "format"        => "image/gif",
 		 "created"       => Time.parse('2013-01-15 19:15:35 UTC'),
-		 "title"         => "spec/data/files/trusttom.GIF"}
+		 "title"         => "trusttom.GIF"}
 	].freeze
 
 end # Thingfish::SpecConstants
 	basedir = Pathname.new( __FILE__ ).dirname.parent
 	strelkadir = basedir.parent + 'Strelka'
 	strelkalibdir = strelkadir + 'lib'
+	mongrel2dir = basedir.parent + 'Mongrel2'
+	mongrel2libdir = mongrel2dir + 'lib'
 
 	$LOAD_PATH.unshift( strelkalibdir.to_s ) unless $LOAD_PATH.include?( strelkalibdir.to_s )
+	$LOAD_PATH.unshift( mongrel2libdir.to_s ) unless $LOAD_PATH.include?( mongrel2libdir.to_s )
 }
 
 # SimpleCov test coverage reporting; enable this using the :coverage rake task

spec/thingfish/metastore/memory_spec.rb

 	end
 
 
+	it "can save a single metadata value for a given oid" do
+		test_val = TEST_METADATA.first['useragent']
+		@store.save( TEST_UUID, 'useragent', test_val )
+		expect( @store.fetch(TEST_UUID, :useragent) ).to eq( TEST_METADATA.first[:useragent] )
+	end
+
+
 	it "can fetch a single metadata value for a given oid" do
 		@store.save( TEST_UUID, TEST_METADATA.first )
 		expect( @store.fetch(TEST_UUID, :format) ).to eq( TEST_METADATA.first[:format] )

spec/thingfish_spec.rb

 		end
 
 
-		it "can fetch a value for a specific metadata key" do
+		it "can fetch a value for a single metadata key" do
 			uuid = handler.datastore.save( @png_io )
 			handler.metastore.save( uuid, {
 				'format' => 'image/png',
 		end
 
 
-		it "returns a 404 Not Found when fetching a specific metadata value for a uuid that doesn't exist" do
+		it "returns a 404 Not Found when fetching a single metadata value for a uuid that doesn't exist" do
 			req = factory.get( "/#{TEST_UUID}/metadata/extent" )
 			result = handler.handle( req )
 
 		end
 
 
-		it "doesn't error when fetching a nonexistent metadata value" do
+		it "doesn't error when fetching a non-existent metadata value" do
 			uuid = handler.datastore.save( @png_io )
 			handler.metastore.save( uuid, {
 				'format' => 'image/png',
 		end
 
 
-		it "can merge in new metadata for an existing resource" do
+		it "can merge in new metadata for an existing resource with a POST" do
 			uuid = handler.datastore.save( @png_io )
 			handler.metastore.save( uuid, {
 				'format' => 'image/png',
 		end
 
 
-		it "doesn't clobber protected metadata when merging metadata" do
+		it "returns FORBIDDEN when attempting to merge metadata with protected keys" do
 			uuid = handler.datastore.save( @png_io )
 			handler.metastore.save( uuid, {
 				'format' => 'image/png',
 			expect( handler.metastore.fetch(uuid, 'format') ).to eq( 'image/png' )
 		end
 
+
+		it "can create single metadata values with a POST" do
+			uuid = handler.datastore.save( @png_io )
+			handler.metastore.save( uuid, {
+				'format' => 'image/png',
+				'extent' => 288,
+			})
+
+			req = factory.post( "/#{uuid}/metadata/comment", "urrrg", 'Content-type' => 'text/plain' )
+			result = handler.handle( req )
+
+			expect( result.status ).to eq( HTTP::CREATED )
+			expect( result.headers.location ).to match( %r|#{uuid}/metadata/comment$| )
+			expect( handler.metastore.fetch(uuid, 'comment') ).to eq( 'urrrg' )
+		end
+
+
+		it "returns NOT_FOUND when attempting to create metadata for a non-existent object" do
+			req = factory.post( "/#{TEST_UUID}/metadata/comment", "urrrg", 'Content-type' => 'text/plain' )
+			result = handler.handle( req )
+
+			expect( result.status ).to eq( HTTP::NOT_FOUND )
+			expect( result.body.string ).to match( /no such object/i )
+		end
+
+
+		it "returns CONFLICT when attempting to create a single metadata value if it already exists" do
+			uuid = handler.datastore.save( @png_io )
+			handler.metastore.save( uuid, {
+				'format'  => 'image/png',
+				'extent'  => 288,
+				'comment' => 'nill bill'
+			})
+
+			req = factory.post( "/#{uuid}/metadata/comment", "urrrg", 'Content-type' => 'text/plain' )
+			result = handler.handle( req )
+
+			expect( result.status ).to eq( HTTP::CONFLICT )
+			expect( result.body.string ).to match( /already exists/i )
+			expect( handler.metastore.fetch(uuid, 'comment') ).to eq( 'nill bill' )
+		end
+
+
+		it "can create single metadata values with a PUT" do
+			uuid = handler.datastore.save( @png_io )
+			handler.metastore.save( uuid, {
+				'format' => 'image/png',
+				'extent' => 288,
+			})
+
+			req = factory.put( "/#{uuid}/metadata/comment", "urrrg", 'Content-type' => 'text/plain' )
+			result = handler.handle( req )
+
+			expect( result.status ).to eq( HTTP::CREATED )
+			expect( result.headers.location ).to match( %r|#{uuid}/metadata/comment$| )
+			expect( handler.metastore.fetch(uuid, 'comment') ).to eq( 'urrrg' )
+		end
+
+
+		it "can replace a single metadata value with a PUT" do
+			uuid = handler.datastore.save( @png_io )
+			handler.metastore.save( uuid, {
+				'format'  => 'image/png',
+				'extent'  => 288,
+				'comment' => 'nill bill'
+			})
+
+			req = factory.put( "/#{uuid}/metadata/comment", "urrrg", 'Content-type' => 'text/plain' )
+			result = handler.handle( req )
+
+			expect( result.status ).to eq( HTTP::NO_CONTENT )
+			expect( handler.metastore.fetch(uuid, 'comment') ).to eq( 'urrrg' )
+		end
+
+
+		it "returns FORBIDDEN when attempting to replace a protected metadata value with a PUT" do
+			uuid = handler.datastore.save( @png_io )
+			handler.metastore.save( uuid, {
+				'format'  => 'image/png',
+				'extent'  => 288,
+				'comment' => 'nill bill'
+			})
+
+			req = factory.put( "/#{uuid}/metadata/format", "image/gif", 'Content-type' => 'text/plain' )
+			result = handler.handle( req )
+
+			expect( result.status ).to eq( HTTP::FORBIDDEN )
+			expect( result.body.string ).to match( /protected metadata/i )
+			expect( handler.metastore.fetch(uuid, 'format') ).to eq( 'image/png' )
+		end
 	end
 end