Commits

Michael Granger  committed 8b98cbf

Add Metastore#keys, #each_key, and #search.

  • Participants
  • Parent commits ad1eb3e

Comments (0)

Files changed (6)

File lib/thingfish/metastore.rb

 
 	# AbstractClass API -- register some virtual methods that must be implemented
 	# in subclasses
-	pure_virtual :save,
+	pure_virtual :keys,
+	             :each_key,
+	             :save,
+	             :search,
 	             :fetch,
 	             :merge,
 	             :include?,

File lib/thingfish/metastore/memory.rb

 	end
 
 
+	##
+	# The raw Hash of metadata
+	attr_reader :storage
+
+
+	### Return an Array of all of the store's keys.
+	def keys
+		return @storage.keys
+	end
+
+
+	### Iterate over each of the store's keys, yielding to the block if one is given
+	### or returning an Enumerator if one is not.
+	def each_key( &block )
+		return @storage.each_key( &block )
+	end
+
+
 	### Save the +metadata+ for the specified +oid+.
 	def save( oid, metadata )
 		oid = self.normalize_oid( oid )
-		self.log.debug "Saving %d metadata attributes for OID %s" % [ metadata.length, oid ]
+		self.log.debug "Saving %d metadata attributes for OID %s: %s" %
+			[ metadata.length, oid, metadata.keys.join(',') ]
 		@storage[ oid ] = metadata.dup
 	end
 
 	end
 
 
+	### Search the metastore for UUIDs which match the specified +criteria+ and
+	### return them as an iterator.
+	def search( criteria={} )
+		ds = @storage.each_key
+
+		if order_fields = criteria[:order]
+			fields = order_fields.split( /\s*,\s*/ )
+			ds = ds.order_by {|uuid| @storage[uuid].values_at(*fields) }
+		end
+
+		ds = ds.reverse if criteria[:direction] && criteria[:direction] == 'desc'
+
+		if (( limit = criteria[:limit] ))
+			offset = criteria[:offset] || 0
+			ds = ds.to_a.slice( offset, limit )
+		end
+
+		return ds.to_a
+	end
+
+
 	### Update the metadata for the given +oid+ with the specified +values+ hash.
 	def merge( oid, values )
 		oid = self.normalize_oid( oid )

File spec/constants.rb

+# -*- ruby -*-
+#encoding: utf-8
+
+require 'thingfish' unless defined?( Thingfish )
+
+module Thingfish::SpecConstants
+	TEST_APPID        = 'thingfish-test'
+	TEST_SEND_SPEC    = 'tcp://127.0.0.1:9999'
+	TEST_RECV_SPEC    = 'tcp://127.0.0.1:9998'
+
+	UUID_PATTERN      = /(?<uuid>[[:xdigit:]]{8}(?:-[[:xdigit:]]{4}){3}-[[:xdigit:]]{12})/i
+
+	TEST_UUID         = 'E5DFEEAB-3525-4F14-B4DB-2772D0B9987F'
+
+	TEST_TEXT_DATA    = "Pork sausage. Pork! Sausage!"
+	TEST_TEXT_DATA_IO = StringIO.new( TEST_TEXT_DATA )
+	TEST_PNG_DATA     = ("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMA" +
+	                     "AQAABQABDQottAAAAABJRU5ErkJggg==").unpack('m').first
+	TEST_PNG_DATA_IO  = StringIO.new( TEST_PNG_DATA )
+
+	TEST_METADATA = [
+		{"useragent"     => "ChunkersTheClown v2.0",
+		 "extent"        => 1072,
+		 "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"},
+		{"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"},
+		{"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"},
+		{"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"},
+		{"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"},
+		{"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"},
+		{"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"},
+		{"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"},
+		{"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"},
+		{"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"},
+		{"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"},
+		{"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"},
+		{"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"},
+		{"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"},
+		{"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"},
+		{"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"},
+		{"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"},
+		{"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"}
+	].freeze
+
+end # Thingfish::SpecConstants
+

File spec/helpers.rb

 	end
 end
 
+require_relative 'constants'
+
 require 'loggability'
 require 'loggability/spechelpers'
 require 'configurability'
 
 ### RSpec helper functions.
 module Thingfish::SpecHelpers
-	TEST_APPID        = 'thingfish-test'
-	TEST_SEND_SPEC    = 'tcp://127.0.0.1:9999'
-	TEST_RECV_SPEC    = 'tcp://127.0.0.1:9998'
-
-	UUID_PATTERN      = /(?<uuid>[[:xdigit:]]{8}(?:-[[:xdigit:]]{4}){3}-[[:xdigit:]]{12})/i
-
-	TEST_UUID         = 'E5DFEEAB-3525-4F14-B4DB-2772D0B9987F'
-
-	TEST_TEXT_DATA    = "Pork sausage. Pork! Sausage!"
-	TEST_TEXT_DATA_IO = StringIO.new( TEST_TEXT_DATA )
-	TEST_PNG_DATA     = ("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMA" +
-	                     "AQAABQABDQottAAAAABJRU5ErkJggg==").unpack('m').first
-	TEST_PNG_DATA_IO  = StringIO.new( TEST_PNG_DATA )
-
+	include Thingfish::SpecConstants
 
 	RSpec::Matchers.define :be_a_uuid do |expected|
 		match do |actual|

File spec/thingfish/metastore/memory_spec.rb

 
 describe Thingfish::Metastore, "memory" do
 
-	TEST_METADATA = {
-		format: 'image/png',
-		extent: 14417
-	}.freeze
-
-
 	before( :all ) do
 		setup_logging()
 	end
 
 	before( :each ) do
-		@store = Thingfish::Metastore.create(:memory)
+		@store = Thingfish::Metastore.create( :memory )
 	end
 
-
 	it "can save and fetch data" do
-		@store.save( TEST_UUID, TEST_METADATA )
-		expect( @store.fetch(TEST_UUID) ).to eq( TEST_METADATA )
-		expect( @store.fetch(TEST_UUID) ).to_not be( TEST_METADATA )
+		@store.save( TEST_UUID, TEST_METADATA.first )
+		expect( @store.fetch(TEST_UUID) ).to eq( TEST_METADATA.first )
+		expect( @store.fetch(TEST_UUID) ).to_not be( TEST_METADATA.first )
 	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 )
+		@store.save( TEST_UUID.downcase, TEST_METADATA.first )
+		expect( @store.fetch(TEST_UUID) ).to eq( TEST_METADATA.first )
 	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] )
+		@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] )
 	end
 
 
 	it "can fetch a slice of data for a given oid" do
-		@store.save( TEST_UUID, TEST_METADATA )
+		@store.save( TEST_UUID, TEST_METADATA.first )
 		expect( @store.fetch(TEST_UUID, :format, :extent) )
-			.to eq( TEST_METADATA.values_at(:format, :extent) )
+			.to eq( TEST_METADATA.first.values_at(:format, :extent) )
 	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] )
+		@store.save( TEST_UUID, TEST_METADATA.first )
+		expect( @store.fetch(TEST_UUID.downcase, :format) ).to eq( TEST_METADATA.first[:format] )
 	end
 
 
 	it "can update data" do
-		@store.save( TEST_UUID, TEST_METADATA )
+		@store.save( TEST_UUID, TEST_METADATA.first )
 		@store.merge( TEST_UUID, format: '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.save( TEST_UUID, TEST_METADATA.first )
 		@store.merge( TEST_UUID.downcase, format: 'image/jpeg' )
 
 		expect( @store.fetch(TEST_UUID, :format) ).to eq( 'image/jpeg' )
 	end
 
+
 	it "can remove metadata for a UUID" do
-		@store.save( TEST_UUID, TEST_METADATA )
+		@store.save( TEST_UUID, TEST_METADATA.first )
 		@store.remove( TEST_UUID )
 
 		expect( @store.fetch(TEST_UUID) ).to be_nil
 	end
 
+
 	it "knows if it has data for a given OID" do
-		@store.save( TEST_UUID, TEST_METADATA )
+		@store.save( TEST_UUID, TEST_METADATA.first )
 		expect( @store ).to include( TEST_UUID )
 	end
 
+
 	it "knows how many objects it contains" do
 		expect( @store.size ).to eq( 0 )
-		@store.save( TEST_UUID, TEST_METADATA )
+		@store.save( TEST_UUID, TEST_METADATA.first )
 		expect( @store.size ).to eq( 1 )
 	end
 
+
+	context "with some uploaded metadata" do
+
+		before( :each ) do
+			@uuids = []
+			TEST_METADATA.each do |file|
+				uuid = SecureRandom.uuid
+				@uuids << uuid
+				@store.save( uuid, file )
+			end
+		end
+
+
+		it "can fetch an array of all of its keys" do
+			expect( @store.keys ).to eq( @uuids )
+		end
+
+		it "can iterate over each of the store's keys" do
+			uuids = []
+			@store.each_key {|u| uuids << u }
+
+			expect( uuids ).to eq( @uuids )
+		end
+
+		it "can provide an enumerator over each of the store's keys" do
+			expect( @store.each_key.to_a ).to eq( @uuids )
+		end
+
+		it "can search for uuids" do
+			expect( @store.search.to_a ).to eq( @store.keys )
+		end
+
+		it "can limit the number of results returned from a search" do
+			expect( @store.search( limit: 2 ).to_a ).to eq( @store.keys[0,2] )
+		end
+
+	end
+
 end
 
 # vim: set nosta noet ts=4 sw=4 ft=rspec:

File spec/thingfish/metastore_spec.rb

 
 		let( :store ) { described_class.create('testing') }
 
+		it "raises an error if it doesn't implement #keys" do
+			expect { store.keys }.to raise_error( NotImplementedError, /keys/ )
+		end
+
+		it "raises an error if it doesn't implement #each_key" do
+			expect { store.each_key }.to raise_error( NotImplementedError, /each_key/ )
+		end
+
 		it "raises an error if it doesn't implement #fetch" do
 			expect { store.fetch(TEST_UUID) }.to raise_error( NotImplementedError, /fetch/ )
 		end
 
+		it "raises an error if it doesn't implement #search" do
+			expect { store.search(limit: 100) }.to raise_error( NotImplementedError, /search/ )
+		end
+
 		it "raises an error if it doesn't implement #save" do
 			expect {
 				store.save( TEST_UUID, {name: 'foo'} )