Commits

Michael Granger committed 5ef34d8

Implement PUT /<uuid>/metadata; add transactional blocks to the stores.

  • Participants
  • Parent commits 07a46ba

Comments (0)

Files changed (8)

File lib/thingfish.rb

 			uri = base_uri.dup
 			uri.path += uuid
 
-			protected_values = self.metastore.fetch( uuid, *OPERATIONAL_METADATA_KEYS )
-			metadata = Hash[ [OPERATIONAL_METADATA_KEYS, protected_values].transpose ]
+			metadata = self.metastore.fetch( uuid, *OPERATIONAL_METADATA_KEYS )
 			metadata['uri'] = uri.to_s
 
 			metadata
 
 		res = req.response
 		res.status = HTTP::OK
-		res.for( :json, :yaml ) { self.metastore.fetch( uuid, key ) }
+		res.for( :json, :yaml ) { self.metastore.fetch_value( uuid, key ) }
 
 		return res
 	end
 
 		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.fetch_value( uuid, key ).nil?
 
-		self.metastore.save( uuid, key, req.body.read )
+		self.metastore.merge( uuid, key => req.body.read )
 
 		res = req.response
 		res.headers.location = req.uri.to_s
 		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 )
+		self.metastore.merge( uuid, key => req.body.read )
 
 		res = req.response
 		res.body = nil
 	end
 
 
+	# PUT /«uuid»/metadata
+	# Replace user metadata for «uuid».
+	put ':uuid/metadata' do |req|
+		uuid = req.params[:uuid]
+
+		finish_with( HTTP::NOT_FOUND, "No such object." ) unless self.metastore.include?( uuid )
+
+		op_metadata = self.metastore.fetch( uuid, *OPERATIONAL_METADATA_KEYS )
+		new_metadata = self.extract_metadata( req )
+		self.metastore.save( uuid, new_metadata.merge(op_metadata) )
+
+		res = req.response
+		res.status = HTTP::NO_CONTENT
+
+		return res
+	end
+
+
 	# POST /«uuid»/metadata
 	# Merge new metadata into the existing metadata for «uuid».
 	post ':uuid/metadata' do |req|

File lib/thingfish/datastore.rb

 		]
 	end
 
+
+	### Provide transactional consistency to the provided block. Concrete datastores should
+	### override this if they can implement it. By default it's a no-op.
+	def transaction
+		yield
+	end
+
 end # class Thingfish::Datastore
 

File lib/thingfish/metastore.rb

 	             :save,
 	             :search,
 	             :fetch,
+	             :fetch_value,
 	             :merge,
 	             :include?,
 	             :remove,
 		]
 	end
 
+
+	### Provide transactional consistency to the provided block. Concrete metastores should
+	### override this if they can implement it. By default it's a no-op.
+	def transaction
+		yield
+	end
+
+
 end # class Thingfish::Metastore
 

File lib/thingfish/metastore/memory.rb

 	end
 
 
-	### 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 )
+	### Save the +metadata+ Hash for the specified +oid+.
+	def save( oid, metadata )
 		oid = self.normalize_oid( oid )
-
-		metadata = args.shift
-
-		if metadata.is_a?( Hash )
-			@storage[ oid ] = metadata.dup
-		else
-			@storage[ oid ] ||= {}
-			@storage[ oid ][ metadata.to_s ] = args.shift
-		end
+		@storage[ oid ] = metadata.dup
 	end
 
 
 	### Fetch the data corresponding to the given +oid+ as a Hash-ish object.
 	def fetch( oid, *keys )
 		oid = self.normalize_oid( oid )
+		metadata = @storage[ oid ] or return nil
+
 		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 ]
+			return metadata.dup
 		else
 			self.log.debug "Fetching metadata for %p for OID %s" % [ keys, oid ]
-			data = @storage[ oid ] or return nil
-			return data.values_at( *keys )
+			values = metadata.values_at( *keys )
+			return Hash[ [keys, values].transpose ]
 		end
 	end
 
 
+	### Fetch the value of the metadata associated with the given +key+ for the
+	### specified +oid+.
+	def fetch_value( oid, key )
+		oid = self.normalize_oid( oid )
+		data = @storage[ oid ] or return nil
+
+		return data[ key ]
+	end
+
+
 	### Search the metastore for UUIDs which match the specified +criteria+ and
 	### return them as an iterator.
 	def search( criteria={} )

File spec/thingfish/datastore_spec.rb

 			expect { store.remove(TEST_UUID) }.to raise_error( NotImplementedError, /remove/ )
 		end
 
+		it "provides a transactional block method" do
+			val = nil
+			store.transaction { val = :yielded }
+			expect( val ).to eq( :yielded )
+		end
+
 	end
 
 end

File spec/thingfish/metastore/memory_spec.rb

 		@store = Thingfish::Metastore.create( :memory )
 	end
 
+
 	it "can save and fetch data" do
 		@store.save( TEST_UUID, TEST_METADATA.first )
 		expect( @store.fetch(TEST_UUID) ).to eq( TEST_METADATA.first )
 	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] )
-		expect( @store.fetch(TEST_UUID, :extent) ).to eq( TEST_METADATA.first[:extent] )
+		expect( @store.fetch_value(TEST_UUID, :format) ).to eq( TEST_METADATA.first[:format] )
+		expect( @store.fetch_value(TEST_UUID, :extent) ).to eq( TEST_METADATA.first[:extent] )
 	end
 
 
 	it "can fetch a slice of data for a given oid" do
 		@store.save( TEST_UUID, TEST_METADATA.first )
-		expect( @store.fetch(TEST_UUID, :format, :extent) )
-			.to eq( TEST_METADATA.first.values_at(:format, :extent) )
+		expect( @store.fetch(TEST_UUID, :format, :extent) ).to eq({
+			:format => TEST_METADATA.first[:format],
+			:extent => TEST_METADATA.first[: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
+		expect( @store.fetch_value(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.first )
-		expect( @store.fetch(TEST_UUID.downcase, :format) ).to eq( TEST_METADATA.first[:format] )
+		expect( @store.fetch_value(TEST_UUID.downcase, :format) ).to eq( TEST_METADATA.first[:format] )
 	end
 
 
 		@store.save( TEST_UUID, TEST_METADATA.first )
 		@store.merge( TEST_UUID, format: 'image/jpeg' )
 
-		expect( @store.fetch(TEST_UUID, :format) ).to eq( 'image/jpeg' )
+		expect( @store.fetch_value(TEST_UUID, :format) ).to eq( 'image/jpeg' )
 	end
 
 
 		@store.save( TEST_UUID, TEST_METADATA.first )
 		@store.merge( TEST_UUID.downcase, format: 'image/jpeg' )
 
-		expect( @store.fetch(TEST_UUID, :format) ).to eq( 'image/jpeg' )
+		expect( @store.fetch_value(TEST_UUID, :format) ).to eq( 'image/jpeg' )
 	end
 
 
 		@store.save( TEST_UUID, TEST_METADATA.first )
 		@store.remove( TEST_UUID, :useragent )
 
-		expect( @store.fetch(TEST_UUID, :useragent) ).to be_nil
+		expect( @store.fetch_value(TEST_UUID, :useragent) ).to be_nil
 	end
 
 

File spec/thingfish/metastore_spec.rb

 			expect { store.fetch(TEST_UUID) }.to raise_error( NotImplementedError, /fetch/ )
 		end
 
+		it "raises an error if it doesn't implement #fetch_value" do
+			expect { store.fetch_value(TEST_UUID, :format) }.
+				to raise_error( NotImplementedError, /fetch_value/ )
+		end
+
 		it "raises an error if it doesn't implement #search" do
 			expect { store.search(limit: 100) }.to raise_error( NotImplementedError, /search/ )
 		end
 			expect { store.size }.to raise_error( NotImplementedError, /size/ )
 		end
 
+		it "provides a transactional block method" do
+			val = nil
+			store.transaction { val = :yielded }
+			expect( val ).to eq( :yielded )
+		end
 	end
 
 end

File spec/thingfish_spec.rb

 			expect( res.headers.location.to_s ).to match( %r:/#{UUID_PATTERN}$: )
 
 			uuid = res.headers.location.to_s[ %r:/(?<uuid>#{UUID_PATTERN})$:, :uuid ]
-			expect( handler.metastore.fetch(uuid, 'title') ).to eq( 'Muffin the Panda Goes To School' )
-			expect( handler.metastore.fetch(uuid, 'tags') ).to eq( 'rapper,ukraine,potap' )
+			expect( handler.metastore.fetch_value(uuid, 'title') ).
+				to eq( 'Muffin the Panda Goes To School' )
+			expect( handler.metastore.fetch_value(uuid, 'tags') ).to eq( 'rapper,ukraine,potap' )
 		end
 
 		it 'replaces content via PUT' do
 			result = handler.handle( req )
 
 			expect( result.status ).to eq( HTTP::NO_CONTENT )
-			expect( handler.metastore.fetch(uuid, 'comment') ).to eq( 'Ignore me!' )
+			expect( handler.metastore.fetch_value(uuid, 'comment') ).to eq( 'Ignore me!' )
 		end
 
 
 			expect( result.status ).to eq( HTTP::FORBIDDEN )
 			expect( result.body.string ).to match( /unable to alter protected metadata/i )
 			expect( result.body.string ).to match( /format/i )
-			expect( handler.metastore.fetch(uuid, 'comment') ).to be_nil
-			expect( handler.metastore.fetch(uuid, 'format') ).to eq( 'image/png' )
+			expect( handler.metastore.fetch_value(uuid, 'comment') ).to be_nil
+			expect( handler.metastore.fetch_value(uuid, 'format') ).to eq( 'image/png' )
 		end
 
 
 
 			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' )
+			expect( handler.metastore.fetch_value(uuid, 'comment') ).to eq( 'urrrg' )
 		end
 
 
 
 			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' )
+			expect( handler.metastore.fetch_value(uuid, 'comment') ).to eq( 'nill bill' )
 		end
 
 
 			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' )
+			expect( result.status ).to eq( HTTP::NO_CONTENT )
+			expect( handler.metastore.fetch_value(uuid, 'comment') ).to eq( 'urrrg' )
 		end
 
 
 			result = handler.handle( req )
 
 			expect( result.status ).to eq( HTTP::NO_CONTENT )
-			expect( handler.metastore.fetch(uuid, 'comment') ).to eq( 'urrrg' )
+			expect( handler.metastore.fetch_value(uuid, 'comment') ).to eq( 'urrrg' )
 		end
 
 
 
 			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' )
+			expect( handler.metastore.fetch_value(uuid, 'format') ).to eq( 'image/png' )
+		end
+
+
+		it "can replace all metadata with a PUT" do
+			uuid = handler.datastore.save( @png_io )
+			handler.metastore.save( uuid, {
+				'format'    => 'image/png',
+				'extent'    => 288,
+				'comment'   => 'nill bill',
+				'ephemeral' => 'butterflies',
+			})
+
+			req = factory.put( "/#{uuid}/metadata", %[{"comment":"Yeah."}],
+			                   'Content-type' => 'application/json' )
+			result = handler.handle( req )
+
+			expect( result.status ).to eq( HTTP::NO_CONTENT )
+			expect( handler.metastore.fetch_value(uuid, 'comment') ).to eq( 'Yeah.' )
+			expect( handler.metastore.fetch_value(uuid, 'format') ).to eq( 'image/png' )
+			expect( handler.metastore ).to_not include( 'ephemeral' )
 		end
 
 
 
 			expect( result.status ).to eq( HTTP::NO_CONTENT )
 			expect( result.body.string ).to be_empty
-			expect( handler.metastore.fetch(uuid, 'format') ).to eq( 'image/png' )
-			expect( handler.metastore.fetch(uuid, 'extent') ).to eq( 288 )
-			expect( handler.metastore.fetch(uuid, 'uploadaddress') ).to eq( '127.0.0.1' )
-			expect( handler.metastore.fetch(uuid, 'created') ).to eq( timestamp )
+			expect( handler.metastore.fetch_value(uuid, 'format') ).to eq( 'image/png' )
+			expect( handler.metastore.fetch_value(uuid, 'extent') ).to eq( 288 )
+			expect( handler.metastore.fetch_value(uuid, 'uploadaddress') ).to eq( '127.0.0.1' )
+			expect( handler.metastore.fetch_value(uuid, 'created') ).to eq( timestamp )
 
-			expect( handler.metastore.fetch(uuid, 'comment') ).to be_nil
-			expect( handler.metastore.fetch(uuid, 'useragent') ).to be_nil
+			expect( handler.metastore.fetch_value(uuid, 'comment') ).to be_nil
+			expect( handler.metastore.fetch_value(uuid, 'useragent') ).to be_nil
 		end
 
 
 
 			expect( result.status ).to eq( HTTP::NO_CONTENT )
 			expect( result.body.string ).to be_empty
-			expect( handler.metastore.fetch(uuid, 'comment') ).to be_nil
-			expect( handler.metastore.fetch(uuid, 'format') ).to eq( 'image/png' )
+			expect( handler.metastore.fetch_value(uuid, 'comment') ).to be_nil
+			expect( handler.metastore.fetch_value(uuid, 'format') ).to eq( 'image/png' )
 		end
 
 
 
 			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' )
+			expect( handler.metastore.fetch_value(uuid, 'format') ).to eq( 'image/png' )
 		end
 	end
 end