Commits

Michael Granger  committed c9b1044

Checkpoint of initial work

  • Participants
  • Parent commits 33bbf50

Comments (0)

Files changed (9)

+$LOAD_PATH.unshift 'lib'
+require 'thingfish'
+
+ThingFish API -- Desired
+=================================================
+
+# introspection
+OPTIONS /v1
+
+# Search (via params), fetch all assets
+GET /v1
+
+# fetch an asset body
+GET /v1/«uuid»
+
+# create a new asset
+POST /v1
+
+# update (replace) an asset body
+PUT /v1/«uuid»
+
+# remove an asset and its metadata
+DELETE /v1/«uuid»
+
+# retrieve all metadata associated with an asset
+GET /v1/«uuid»/metadata
+
+# retrieve values for an asset's metadata key
+GET /v1/«uuid»/metadata/«key»
+
+# append additional metadata for an asset
+POST /v1/«uuid»/metadata
+
+# add a value for an asset's specific 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»
+
+# remove all user metadata for an asset
+DELETE /v1/«uuid»/metadata
+
+# delete an asset's specific metadata key
+DELETE /v1/«uuid»/metadata/«key»
+
+

File lib/thingfish.rb

 #!/usr/bin/env ruby
 
-require 'loggability'
-require 'configurability'
+require 'strelka'
+require 'strelka/app'
 
 #
-# Network-accessable searchable datastore
+# Network-accessable datastore service
 #
 # == Version
 #
 # * Michael Granger <ged@FaerieMUD.org>
 # * Mahlon E. Smith <mahlon@martini.nu>
 #
-module ThingFish
-	extend Loggability
+class ThingFish < Strelka::App
+
+	# Loggability API -- log all ThingFish-related stuff to a separate logger
+	log_as :thingfish
 
 
 	# Package version
 	# Version control revision
 	REVISION = %q$Revision$
 
+	# Configurability API -- set config defaults
+	CONFIG_DEFAULTS = {
+		datastore: 'memory',
+		metastore: 'memory'
+	}
+
+	require 'thingfish/mixins'
+	extend MethodUtilities
+
 
 	### Get the library version. If +include_buildnum+ is true, the version string will
 	### include the VCS rev ID.
 		return vstring
 	end
 
-end # module ThingFish
+
+	##
+	# Configurability API
+	config_key :thingfish
+
+	# The configured datastore type
+	singleton_attr_accessor :datastore
+
+	# The configured metastore type
+	singleton_attr_accessor :metastore
+
+
+	### Configurability API -- install the configuration
+	def self::configure( config=nil )
+		config ||= self.defaults
+		self.datastore = config[:datastore] || 'memory'
+		self.metastore = config[:metastore] || 'memory'
+	end
+
+
+	### Set up blerghh...
+	def initialize( * )
+		super
+
+		@datastore = ThingFish::Datastore.create( self.class.datastore )
+		# @metastore = ThingFish::Metastore.create( self.class.metastore )
+	end
+
+
+	######
+	public
+	######
+
+	# The datastore
+	attr_reader :datastore
+
+	#
+	# Global parmas
+	#
+	plugin :parameters
+	param :uuid
+
+
+	#
+	# Routing
+	#
+	plugin :routing
+	router :exclusive
+
+	# GET /«uuid»
+	# Fetch an object by ID
+	get ':uuid' do |req|
+		uuid = req.params[:uuid]
+		object = self.datastore.fetch_object( uuid ) or
+			finish_with HTTP::NOT_FOUND, "no such object"
+
+		res = req.response
+		res.body = object
+
+		return res
+	end
+
+
+	# POST /
+	# Upload a new object.
+	post do |req|
+		uuid = self.datastore.create_object( req.body )
+		url = req.base_uri.dup
+		url.path += uuid
+
+		res = req.response
+		res.headers.location = url
+		res.status = HTTP::CREATED
+
+		return res
+	end
+
+
+	require 'thingfish/datastore'
+
+end # class ThingFish
 
 # vim: set nosta noet ts=4 sw=4:

File lib/thingfish/datastore.rb

+# -*- ruby -*-
+#encoding: utf-8
+
+require 'securerandom'
+require 'pluggability'
+require 'stringio'
+
+require 'thingfish' unless defined?( ThingFish )
+require 'thingfish/mixins'
+
+# The base class for storage mechanisms used by ThingFish to store its data
+# blobs.
+class ThingFish::Datastore
+	extend Pluggability,
+	       ThingFish::AbstractClass
+	include Enumerable
+
+
+	# Pluggability API -- set the prefix for implementations of Datastore
+	plugin_prefixes 'thingfish/datastore'
+
+	# AbstractClass API -- register some virtual methods that must be implemented
+	# in subclasses
+	pure_virtual :create_object,
+	             :fetch_object,
+	             :each
+
+	alias_method :[]=, :create_object
+	alias_method :[], :fetch_object
+
+
+	#########
+	protected
+	#########
+
+	### Generate a new object ID.
+	def make_object_id
+		return SecureRandom.uuid
+	end
+
+end # class ThingFish::Datastore
+

File lib/thingfish/datastore/memory.rb

+# -*- ruby -*-
+#encoding: utf-8
+
+require 'thingfish' unless defined?( ThingFish )
+require 'thingfish/datastore' unless defined?( ThingFish::Datastore )
+
+
+
+# An in-memory datastore for testing and tryout purposes.
+class ThingFish::MemoryDatastore < ThingFish::Datastore
+	extend Loggability
+
+
+	# Loggability API -- log to the :thingfish logger
+	log_to :thingfish
+
+
+
+end # class ThingFish::MemoryDatastore
+

File lib/thingfish/mixins.rb

 require 'thingfish' unless defined?( ThingFish )
 
 
-module ThingFish
+class ThingFish
 
 	# Hides your class's ::new method and adds a +pure_virtual+ method generator for
 	# defining API methods. If subclasses of your class don't provide implementations of
 	#
 	module AbstractClass
 
-		### Methods to be added to including classes
-		module ClassMethods
+		### Extension callback -- mark the extended object's .new as private
+		def self::extended( mod )
+			super
+			mod.class_eval { private_class_method :new }
+		end
+
 
 			### Define one or more "virtual" methods which will raise
 			### NotImplementedErrors when called via a concrete subclass.
 			end
 
 
-			### Turn subclasses' new methods back to public.
+		### Inheritance callback -- Turn subclasses' .new methods back to public.
 			def inherited( subclass )
 				subclass.module_eval { public_class_method :new }
 				super
 			end
 
-		end # module ClassMethods
-
-
-		### Inclusion callback
-		def self::included( mod )
-			super
-			if mod.respond_to?( :new )
-				mod.extend( ClassMethods )
-				mod.module_eval { private_class_method :new }
-			end
-		end
-
-
 	end # module AbstractClass
 
 
-	# A collection of various delegation code-generators that can be used to define
-	# delegation through other methods, to instance variables, etc.
-	module Delegation
-
-		###############
-		module_function
-		###############
-
-		### Define the given +delegated_methods+ as delegators to the like-named method
-		### of the return value of the +delegate_method+.
-	###
-		###    class MyClass
-		###      extend ThingFish::Delegation
-		###
-		###      # Delegate the #bound?, #err, and #result2error methods to the connection
-		###      # object returned by the #connection method. This allows the connection
-		###      # to still be loaded on demand/overridden/etc.
-		###      def_method_delegators :connection, :bound?, :err, :result2error
-		###
-		###      def connection
-		###        @connection ||= self.connect
-		###      end
-		###   end
-		###
-		def def_method_delegators( delegate_method, *delegated_methods )
-			delegated_methods.each do |name|
-				body = make_method_delegator( delegate_method, name )
-				define_method( name, &body )
-			end
-			end
-
-
-		### Define the given +delegated_methods+ as delegators to the like-named method
-		### of the specified +ivar+. This is pretty much identical with how 'Forwardable'
-		### from the stdlib does delegation, but it's reimplemented here for consistency.
-		###
-		###    class MyClass
-		###      extend ThingFish::Delegation
-		###
-		###      # Delegate the #each method to the @collection ivar
-		###      def_ivar_delegators :@collection, :each
-		###
-		###    end
-		###
-		def def_ivar_delegators( ivar, *delegated_methods )
-			delegated_methods.each do |name|
-				body = make_ivar_delegator( ivar, name )
-				define_method( name, &body )
-			end
-			end
-
-
-		### Define the given +delegated_methods+ as delegators to the like-named class
-		### method.
-		def def_class_delegators( *delegated_methods )
-			delegated_methods.each do |name|
-				define_method( name ) do |*args|
-					self.class.__send__( name, *args )
-			end
-			end
-		end
-
-
-		#######
-		private
-		#######
-
-		### Make the body of a delegator method that will delegate to the +name+ method
-		### of the object returned by the +delegate+ method.
-		def make_method_delegator( delegate, name )
-			error_frame = caller(5)[0]
-			file, line = error_frame.split( ':', 2 )
-
-			# Ruby can't parse obj.method=(*args), so we have to special-case setters...
-			if name.to_s =~ /(\w+)=$/
-				name = $1
-				code = <<-END_CODE
-				lambda {|*args| self.#{delegate}.#{name} = *args }
-				END_CODE
-				else
-				code = <<-END_CODE
-				lambda {|*args,&block| self.#{delegate}.#{name}(*args,&block) }
-				END_CODE
-				end
-
-			return eval( code, nil, file, line.to_i )
-				end
-
-
-		### Make the body of a delegator method that will delegate calls to the +name+
-		### method to the given +ivar+.
-		def make_ivar_delegator( ivar, name )
-			error_frame = caller(5)[0]
-			file, line = error_frame.split( ':', 2 )
-
-			# Ruby can't parse obj.method=(*args), so we have to special-case setters...
-			if name.to_s =~ /(\w+)=$/
-				name = $1
-				code = <<-END_CODE
-				lambda {|*args| #{ivar}.#{name} = *args }
-				END_CODE
-			else
-				code = <<-END_CODE
-				lambda {|*args,&block| #{ivar}.#{name}(*args,&block) }
-				END_CODE
-			end
-
-			return eval( code, nil, file, line.to_i )
-		end
-
-	end # module Delegation
-
-
-	# A collection of miscellaneous functions that are useful for manipulating
-	# complex data structures.
-	#
-	#   include ThingFish::DataUtilities
-	#   newhash = deep_copy( oldhash )
-	#
-	module DataUtilities
-
-		###############
-		module_function
-		###############
-
-		### Recursively copy the specified +obj+ and return the result.
-		def deep_copy( obj )
-
-			# Handle mocks during testing
-			return obj if obj.class.name == 'RSpec::Mocks::Mock'
-
-			return case obj
-				when NilClass, Numeric, TrueClass, FalseClass, Symbol, Module
-					obj
-
-				when Array
-					obj.map {|o| deep_copy(o) }
-
-				when Hash
-					newhash = {}
-					newhash.default_proc = obj.default_proc if obj.default_proc
-					obj.each do |k,v|
-						newhash[ deep_copy(k) ] = deep_copy( v )
-					end
-					newhash
-
-			else
-					obj.clone
-				end
-			end
-
-
-		### Create and return a Hash that will auto-vivify any values it is missing with
-		### another auto-vivifying Hash.
-		def autovivify( hash, key )
-			hash[ key ] = Hash.new( &ThingFish::DataUtilities.method(:autovivify) )
-			end
-
-
-		### Return a version of the given +hash+ with its keys transformed
-		### into Strings from whatever they were before.
-		def stringify_keys( hash )
-			newhash = {}
-
-			hash.each do |key,val|
-				if val.is_a?( Hash )
-					newhash[ key.to_s ] = stringify_keys( val )
-				else
-					newhash[ key.to_s ] = val
-				end
-			end
-
-			return newhash
-		end
-
-
-		### Return a duplicate of the given +hash+ with its identifier-like keys
-		### transformed into symbols from whatever they were before.
-		def symbolify_keys( hash )
-			newhash = {}
-
-			hash.each do |key,val|
-				keysym = key.to_s.dup.untaint.to_sym
-
-				if val.is_a?( Hash )
-					newhash[ keysym ] = symbolify_keys( val )
-				else
-					newhash[ keysym ] = val
-				end
-		end
-
-			return newhash
-		end
-		alias_method :internify_keys, :symbolify_keys
-
-	end # module DataUtilities
-
-
 	# A collection of methods for declaring other methods.
 	#
 	#   class MyClass

File spec/helpers.rb

 require 'loggability'
 require 'loggability/spechelpers'
 require 'configurability'
+require 'configurability/behavior'
 
 require 'rspec'
 require 'mongrel2'
 
 ### 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
 end
 
 
 ### Mock with RSpec
 RSpec.configure do |c|
 	include Strelka::Constants
+	include ThingFish::SpecHelpers
 
 	c.treat_symbols_as_metadata_keys_with_true_values = true
 	c.mock_with( :rspec ) do |config|
 
 	c.include( Loggability::SpecHelpers )
 	c.include( Mongrel2::SpecHelpers )
+	c.include( Mongrel2::Constants )
 	c.include( Strelka::Constants )
 	c.include( Strelka::Testing )
 	c.include( ThingFish::SpecHelpers )

File spec/thingfish/datastore_spec.rb

+#!/usr/bin/env ruby
+
+require_relative '../helpers'
+
+require 'rspec'
+require 'thingfish/datastore'
+
+
+describe ThingFish::Datastore do
+
+	before( :all ) do
+		setup_logging()
+	end
+
+
+
+
+end
+
+# vim: set nosta noet ts=4 sw=4 ft=rspec:

File spec/thingfish_spec.rb

 require_relative 'helpers'
 
 require 'rspec'
-
-require 'loggability/spechelpers'
-require 'configurability/behavior'
-
 require 'thingfish'
 
 
 describe ThingFish do
 
+	before( :all ) do
+		setup_logging()
+		ThingFish.configure
+	end
+
+
+	let( :handler ) { described_class.new(TEST_APPID, TEST_SEND_SPEC, TEST_RECV_SPEC) }
+	let( :factory ) { Mongrel2::RequestFactory.new(:route => '/') }
+	let( :text_data ) { "Some stuff." }
+	let( :png_data ) do
+		("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMA" +
+		"AQAABQABDQottAAAAABJRU5ErkJggg==").unpack('m').first
+	end
+
+
+	#
+	# Shared behaviors
+	#
+
+	it_should_behave_like "an object with Configurability"
+
+
+	#
+	# Examples
+	#
 
 	it "returns a version string if asked" do
 		ThingFish.version_string.should =~ /\w+ [\d.]+/
 	end
 
 
+	it 'accepts a POSTed upload' do
+		req = factory.post( '/', 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 "can fetch an uploaded chunk of data" do
+		upload = factory.post( '/', png_data, content_type: 'image/png' )
+		upload_res = handler.handle( upload )
+
+		url = URI( upload_res.headers.location )
+		fetch = factory.get( url.path )
+		result = handler.handle( fetch )
+
+		expect( result.status_line ).to match( /200 ok/i )
+		expect( result.body.read ).to eq( png_data )
+		expect( result.headers.content_type ).to eq( 'image/png' )
+	end
+
+
 end
 
-
 # vim: set nosta noet ts=4 sw=4 ft=rspec: