Index: trunk/Nebulous-Server/Changes
===================================================================
--- trunk/Nebulous-Server/Changes	(revision 18434)
+++ trunk/Nebulous-Server/Changes	(revision 18451)
@@ -2,4 +2,7 @@
 
 0.13
+    - create a Nebulous::Keys object to represent a parsed neb key and
+      impliment other changes to abstract out the handling of keys in order to
+      support "soft" volume requests and the "any" volume
     - change find_objects_with_unavailable_instances query to only count
       volumes marked as 'available'
Index: trunk/Nebulous-Server/lib/Nebulous/Keys.pm
===================================================================
--- trunk/Nebulous-Server/lib/Nebulous/Keys.pm	(revision 18434)
+++ trunk/Nebulous-Server/lib/Nebulous/Keys.pm	(revision 18451)
@@ -1,5 +1,5 @@
 # Copyright (c) 2004  Joshua Hoblitt
 #
-# $Id: Keys.pm,v 1.1 2008-03-20 21:13:28 jhoblitt Exp $
+# $Id: Keys.pm,v 1.2 2008-07-09 23:32:35 jhoblitt Exp $
 
 package Nebulous::Keys;
@@ -10,27 +10,34 @@
 our $VERSION = '0.01';
 
-use base qw( Exporter );
-
-use File::Spec::Functions;
+use base qw( Exporter Class::Accessor::Fast );
+
+use File::Spec;
 use URI::file;
 use URI;
-
-our @EXPORT_OK      = qw( parse_neb_key );
+use overload '""' => \&_stringify_key;
+
+our @EXPORT_OK = qw(
+    parse_neb_key
+    parse_neb_volume
+);
+
+__PACKAGE__->mk_ro_accessors(qw( path volume soft_volume ));
 
 sub parse_neb_key
 {
-    my $text = shift;
-    return unless defined $text;
+    my ($key, $volume) = @_;
+    return unless defined $key;
 
     # white space is not allowed
-    if ($text =~ qr/\s+/) {
+    if ($key =~ qr/\s+/) {
         die "keys and URIs may not contain whitespace";
     }
 
-    my $uri = URI->new($text);
+    my $uri = URI->new($key);
     my $scheme = $uri->scheme;
     my $path = $uri->opaque;
     
-    my $volume;
+    my $volume_name;
+    my $soft_volume;
     # if this is a valid uri 
     if (defined $scheme) {
@@ -47,10 +54,27 @@
         # strip off volume component if it exists
         $path =~ s|^//([^/]+)||;
-        $volume = $1;
+        
+        # ignore key supplied volume name if one is passed as a parameter
+        $volume ||= $1;
+        my $volume_info = parse_neb_volume($volume);
+
+        # magically eat the "any" volume
+        if (defined $volume_info->{volume} and $volume_info->{volume} ne 'any') {
+            $volume_name = $volume_info->{volume};
+            $soft_volume = $volume_info->{soft_volume};
+        }
 
         # require a leading slash if there is no volume name
-        if ((not defined $volume) and (not $path =~ m|^/|)) {
+        if ((not defined $volume_name) and (not $path =~ m|^/|)) {
             die "neb URI scheme requires a leading slash, eg. neb:/";
         }
+    } else {
+        my $volume_info = parse_neb_volume($volume);
+
+        # magically eat the "any" volume
+        if (defined $volume_info->{volume} and $volume_info->{volume} ne 'any') {
+            $volume_name = $volume_info->{volume};
+            $soft_volume = $volume_info->{soft_volume};
+        }
     }
 
@@ -59,7 +83,36 @@
 
     # remove multiple /'s and trailing slashes
-    $path = canonpath($path);
-
-    return ($volume, $path);
+    $path = File::Spec->canonpath($path);
+
+    return __PACKAGE__->new({
+        volume      => $volume_name,
+        soft_volume => $soft_volume,
+        path        => $path,
+    });
+}
+
+sub parse_neb_volume
+{
+    my $volume = shift;
+    return unless defined $volume;
+
+    my $soft_volume;
+    # check to see if there is a tilde and remove it if found
+    if (defined $volume and $volume =~ s/^~//) {
+        $soft_volume = 1;
+    }
+
+    return({ volume => $volume, soft_volume => $soft_volume });
+}
+
+sub _stringify_key
+{
+    my $self = shift;
+
+    my $path        = $self->path;
+    my $volume      = $self->volume || "";
+    my $soft_volume = $self->soft_volume ? '~' : "";
+
+    return "neb://${soft_volume}${volume}/$path";
 }
 
Index: trunk/Nebulous-Server/lib/Nebulous/Server.pm
===================================================================
--- trunk/Nebulous-Server/lib/Nebulous/Server.pm	(revision 18434)
+++ trunk/Nebulous-Server/lib/Nebulous/Server.pm	(revision 18451)
@@ -1,5 +1,5 @@
 # Copyright (c) 2004-2008  Joshua Hoblitt
 #
-# $Id: Server.pm,v 1.79 2008-07-02 03:40:10 jhoblitt Exp $
+# $Id: Server.pm,v 1.80 2008-07-09 23:32:35 jhoblitt Exp $
 
 package Nebulous::Server;
@@ -22,5 +22,5 @@
 use Nebulous::Server::Log;
 use Nebulous::Server::SQL;
-use Nebulous::Keys qw( parse_neb_key );
+use Nebulous::Keys qw( parse_neb_key parse_neb_volume );
 use Params::Validate qw( validate_pos SCALAR SCALARREF UNDEF );
 use URI::file;
@@ -167,16 +167,20 @@
     $log->debug( "entered - @_" );
 
-    {
-        my $volume;
-        ($volume, $key) = parse_neb_key($key);
-        # vol_name overrides the key implied volume
-        $vol_name ||= $volume;
-    }
-
-    if (defined $vol_name and !$self->_is_valid_volume_name($vol_name)) {
-        die "$vol_name is not a valid volume name"
+    $key = parse_neb_key($key, $vol_name);
+    # vol_name overrides the key implied volume
+    $vol_name = $key->volume;
+
+    # the key isn't validiated on input for this method so we have to check it
+    # again after parsing the key
+    if (defined $vol_name and not $self->_is_valid_volume_name($vol_name)) {
+        $log->logdie("$vol_name is not a valid volume name");
     }
         
-    my ($vol_id, $vol_host, $vol_path, $vol_xattr) = $self->_get_storage_volume($vol_name);
+    if (defined $key->soft_volume) {
+        $vol_name = undef;
+    }
+
+    my ($vol_id, $vol_host, $vol_path, $vol_xattr)
+        = $self->_get_storage_volume($vol_name);
 
     my $uri;
@@ -185,5 +189,5 @@
             # create storage_object
             my $query = $db->prepare_cached( $sql->new_object ); 
-            $query->execute('NULL', $key);
+            $query->execute('NULL', $key->path);
         }
 
@@ -231,5 +235,5 @@
 
         # TODO add some stuff here to retry if unsucessful
-        $uri = $self->_create_empty_instance_file($key, $so_id, $ins_id, $vol_path, $vol_xattr);
+        $uri = $self->_create_empty_instance_file($key->path, $so_id, $ins_id, $vol_path, $vol_xattr);
         $log->debug("created $uri on volume ID: $vol_id");
 
@@ -289,5 +293,5 @@
         my $query = $db->prepare_cached($sql->rename_object); 
         # this SQL statment takes the new key name as the first param
-        my $rows = $query->execute($newkey, $key);
+        my $rows = $query->execute($newkey->path, $key->path);
 
         # if we affected more then one row something very bad has happened.
@@ -351,9 +355,10 @@
     $log->debug("entered - @_");
 
-    {
-        my $volume;
-        ($volume, $key) = parse_neb_key($key);
-        # vol_name overrides the key implied volume
-        $vol_name ||= $volume;
+    $key = parse_neb_key($key, $vol_name);
+    # vol_name overrides the key implied volume
+    $vol_name = $key->volume;
+
+    if (defined $key->soft_volume) {
+        $vol_name = undef;
     }
 
@@ -377,5 +382,5 @@
             # verify that at least one instance is currently available
             my $query = $db->prepare_cached( $sql->get_object_instances );
-            my $rows = $query->execute($key, 1);
+            my $rows = $query->execute($key->path, 1);
 
             unless ( $rows > 0 ) {
@@ -409,5 +414,5 @@
 
         # TODO add some stuff here to retry if unsucessful
-        $uri = $self->_create_empty_instance_file($key, $so_id, $ins_id, $vol_path, $vol_xattr);
+        $uri = $self->_create_empty_instance_file($key->path, $so_id, $ins_id, $vol_path, $vol_xattr);
 
         {
@@ -467,5 +472,5 @@
             # this will set update locks
             my $query = $db->prepare_cached( $sql->get_object_locks );
-            my $rows = $query->execute( $key );
+            my $rows = $query->execute( $key->path );
             unless ( $rows == 1 ) {
                 $query->finish;
@@ -494,5 +499,5 @@
             {
                 my $query = $db->prepare_cached( $sql->set_write_lock );
-                my $rows = $query->execute($key);
+                my $rows = $query->execute($key->path);
             
                 # if we affected more then one row something very bad has happened.
@@ -510,5 +515,5 @@
             {
                 my $query = $db->prepare_cached( $sql->increment_read_lock );
-                my $rows = $query->execute($key);
+                my $rows = $query->execute($key->path);
             
                 # if we affected more then one row something very bad has happened.
@@ -569,5 +574,5 @@
             # this will set update locks
             my $query = $db->prepare_cached( $sql->get_object_locks );
-            my $rows = $query->execute($key);
+            my $rows = $query->execute($key->path);
             unless ($rows == 1) {
                 $query->finish;
@@ -595,5 +600,5 @@
             {
                 my $query = $db->prepare_cached( $sql->delete_write_lock );
-                my $rows = $query->execute($key);
+                my $rows = $query->execute($key->path);
             
                 # if we affected more then one row something very bad has happened.
@@ -615,5 +620,5 @@
             {
                 my $query = $db->prepare_cached( $sql->decrement_read_lock );
-                my $rows = $query->execute($key);
+                my $rows = $query->execute($key->path);
             
                 # if we affected more then one row something very bad has happened.
@@ -684,5 +689,5 @@
 
         # name, value, ext_id
-        my $rows = $query->execute($name, $value, $key);
+        my $rows = $query->execute($name, $value, $key->path);
         $query->finish;
 
@@ -744,5 +749,5 @@
         my $query = $db->prepare_cached( $sql->get_object_xattr );
         # ext_id, name
-        my $rows = $query->execute($key, $name);
+        my $rows = $query->execute($key->path, $name);
 
         # no rows returned means that the xattr does not exist
@@ -797,5 +802,5 @@
         my $query = $db->prepare_cached( $sql->list_object_xattr );
         # ext_id
-        my $rows = $query->execute($key);
+        my $rows = $query->execute($key->path);
 
         while (my $row = $query->fetchrow_hashref) {
@@ -839,5 +844,5 @@
         my $query = $db->prepare_cached( $sql->remove_object_xattr );
         # ext_id, name
-        my $rows = $query->execute($key, $name);
+        my $rows = $query->execute($key->path, $name);
         $query->finish;
 
@@ -885,7 +890,5 @@
 
     # attempt to strip off neb:// if it exists
-    if ($pattern =~ m|^neb:|) {
-        $pattern = parse_neb_key($pattern);        
-    }
+    $pattern =~ s|^neb:||;
 
     my @keys;
@@ -939,16 +942,10 @@
     $log->debug("entered - @_");
 
-    {
-        my $volume;
-        ($volume, $key) = parse_neb_key($key);
-        if (defined $volume) {
-            die "$volume is not a valid volume name"
-                unless $self->_is_valid_volume_name($volume);
-        }
-        $vol_name ||= $volume;
-    }
-
-    # handle :any volume
-    if ($vol_name eq ':any') {
+    $key = parse_neb_key($key, $vol_name);
+    # vol_name overrides the key implied volume
+    $vol_name = $key->volume;
+
+    # handle soft volumes
+    if (defined $key->soft_volume) {
         $vol_name = undef;
     }
@@ -960,5 +957,5 @@
             $query = $db->prepare_cached( $sql->get_object_instances_by_vol_name );
             # ext_id, name, available
-            my $rows = $query->execute($key, $vol_name, 1);
+            my $rows = $query->execute($key->path, $vol_name, 1);
             unless ($rows > 0) {
                 $query->finish;
@@ -968,5 +965,5 @@
             $query = $db->prepare_cached( $sql->get_object_instances );
             # ext_id, available
-            my $rows = $query->execute($key, 1);
+            my $rows = $query->execute($key->path, 1);
             unless ($rows > 0) {
                 $query->finish;
@@ -1102,5 +1099,5 @@
     eval {
         my $query = $db->prepare_cached( $sql->stat_object );
-        my $rows = $query->execute($key);
+        my $rows = $query->execute($key->path);
 
         unless ($rows == 1) {
@@ -1228,6 +1225,5 @@
     my $key = shift;
 
-    my $volume;
-    ($volume, $key) = parse_neb_key($key);
+    $key = parse_neb_key($key);
 
     my ($vol_id, $vol_host, $vol_path, $xattr);
@@ -1236,5 +1232,5 @@
         my $query = $db->prepare_cached( $sql->get_replication_volume_for_ext_id );
         # ext_id, %free, avaiable, allocate
-        $rows = $query->execute($key, 0.95, 1, 1);
+        $rows = $query->execute($key->path, 0.95, 1, 1);
         # XXX destinguish between non-existant and unaviable
         unless ($rows > 0) {
@@ -1271,11 +1267,10 @@
     my $db  =$self->db;
 
-    my $volume;
-    ($volume, $key) = parse_neb_key($key);
+    $key = parse_neb_key($key);
 
     my $ext_id;
     eval {
         my $query = $db->prepare_cached( $sql->get_object ); 
-        $query->execute($key);
+        $query->execute($key->path);
         ($ext_id) = $query->fetchrow_array;
         $query->finish;
@@ -1303,6 +1298,10 @@
     my $db  =$self->db;
 
-    # handle ":any" volume
-    if ($vol_name eq ':any') {
+    my $volume_info = parse_neb_volume($vol_name);
+
+    $vol_name = $volume_info->{volume};
+
+    # handle "any" volume
+    if ($vol_name eq 'any') {
         return 1;
     }
Index: trunk/Nebulous-Server/t/07_server_find_instances.t
===================================================================
--- trunk/Nebulous-Server/t/07_server_find_instances.t	(revision 18434)
+++ trunk/Nebulous-Server/t/07_server_find_instances.t	(revision 18451)
@@ -3,5 +3,5 @@
 # Copryight (C) 2004-2005  Joshua Hoblitt
 #
-# $Id: 07_server_find_instances.t,v 1.13 2008-07-02 03:40:10 jhoblitt Exp $
+# $Id: 07_server_find_instances.t,v 1.14 2008-07-09 23:32:35 jhoblitt Exp $
 
 use strict;
@@ -78,5 +78,5 @@
     my $uri = $neb->create_object('foo', 'node01');
 
-    my $locations = $neb->find_instances('neb://node02/foo', ":any");
+    my $locations = $neb->find_instances('neb://node02/foo', "~any");
 
     uri_scheme_ok($locations->[0], 'file');
@@ -91,5 +91,5 @@
     my $uri2 = $neb->replicate_object("foo");
 
-    my $locations = $neb->find_instances("foo", ":any");
+    my $locations = $neb->find_instances("foo", "~any");
 
     uri_scheme_ok($locations->[0], 'file');
Index: trunk/Nebulous-Server/t/14_server_xattr.t
===================================================================
--- trunk/Nebulous-Server/t/14_server_xattr.t	(revision 18434)
+++ trunk/Nebulous-Server/t/14_server_xattr.t	(revision 18451)
@@ -3,5 +3,5 @@
 # Copryight (C) 2007  Joshua Hoblitt
 #
-# $Id: 14_server_xattr.t,v 1.7 2008-05-15 03:28:05 jhoblitt Exp $
+# $Id: 14_server_xattr.t,v 1.8 2008-07-09 23:32:35 jhoblitt Exp $
 
 use strict;
@@ -164,5 +164,5 @@
     $neb->getxattr_object('foo', 'bar');
 };
-like($@, qr/xattr foo:bar does not exist/,
+like($@, qr|xattr neb:///foo:bar does not exist|,
     "get xattr from non-existant xattr key");
 
Index: trunk/Nebulous-Server/t/75_parse_neb_key.t
===================================================================
--- trunk/Nebulous-Server/t/75_parse_neb_key.t	(revision 18434)
+++ trunk/Nebulous-Server/t/75_parse_neb_key.t	(revision 18451)
@@ -3,5 +3,5 @@
 # Copryight (C) 2004-2005  Joshua Hoblitt
 #
-# $Id: 75_parse_neb_key.t,v 1.3 2008-03-21 01:23:45 jhoblitt Exp $
+# $Id: 75_parse_neb_key.t,v 1.4 2008-07-09 23:32:35 jhoblitt Exp $
 
 use strict;
@@ -10,5 +10,5 @@
 use Test::More;
 
-plan tests => 36;
+plan tests => 80;
 
 use lib qw( ./t ./lib );
@@ -24,104 +24,202 @@
 # key
 {
-    my ($volume, $path) = parse_neb_key('foo/bar/baz/quix');
-
-    is($path, 'foo/bar/baz/quix', 'path');
-    is($volume, undef, 'volume name');
-}
-
-{
-    my ($volume, $path) = parse_neb_key('/foo/bar/baz/quix');
-
-    is($path, 'foo/bar/baz/quix', 'path');
-    is($volume, undef, 'volume name');
-}
-
-{
-    my ($volume, $path) = parse_neb_key('//foo/bar/baz/quix');
-
-    is($path, 'foo/bar/baz/quix', 'path');
-    is($volume, undef, 'volume name');
-}
-
-{
-    my ($volume, $path) = parse_neb_key('///foo/bar/baz/quix');
-
-    is($path, 'foo/bar/baz/quix', 'path');
-    is($volume, undef, 'volume name');
-}
-
-{
-    my ($volume, $path) = parse_neb_key('foo////bar/baz/quix');
-
-    is($path, 'foo/bar/baz/quix', 'path');
-    is($volume, undef, 'volume name');
-}
-
-{
-    my ($volume, $path) = parse_neb_key('foo/bar/baz/quix/');
-
-    is($path, 'foo/bar/baz/quix', 'path');
-    is($volume, undef, 'volume name');
-}
-
+    my $key = parse_neb_key('foo/bar/baz/quix');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('/foo/bar/baz/quix');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('//foo/bar/baz/quix');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('///foo/bar/baz/quix');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('foo////bar/baz/quix');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('foo/bar/baz/quix/');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+# key w/ volume argument
+{
+    my $key = parse_neb_key('foo/bar/baz/quix', 'boing');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, 'boing', 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('foo/bar/baz/quix', '~boing');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, 'boing', 'volume name');
+    is($key->soft_volume, 1, 'soft volume name');
+}
 
 # URI w/ volume name
 {
-    my ($volume, $path) = parse_neb_key('neb://foo/bar/baz/quix');
-
-    is($path, 'bar/baz/quix', 'path');
-    is($volume, 'foo', 'volume name');
-}
-
-{
-    my ($volume, $path) = parse_neb_key('neb://foo//bar/baz/quix');
-
-    is($path, 'bar/baz/quix', 'path');
-    is($volume, 'foo', 'volume name');
-}
-
-{
-    my ($volume, $path) = parse_neb_key('neb://foo///bar/baz/quix');
-
-    is($path, 'bar/baz/quix', 'path');
-    is($volume, 'foo', 'volume name');
-}
-
-{
-    my ($volume, $path) = parse_neb_key('neb://foo/bar///baz/quix');
-
-    is($path, 'bar/baz/quix', 'path');
-    is($volume, 'foo', 'volume name');
-}
-
-{
-    my ($volume, $path) = parse_neb_key('neb://foo/bar/baz/quix/');
-
-    is($path, 'bar/baz/quix', 'path');
-    is($volume, 'foo', 'volume name');
-}
-
+    my $key = parse_neb_key('neb://foo/bar/baz/quix');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'foo', 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb://foo//bar/baz/quix');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'foo', 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb://foo///bar/baz/quix');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'foo', 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb://foo/bar///baz/quix');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'foo', 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb://foo/bar/baz/quix/');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'foo', 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+# URI w/ soft volume name
+{
+    my $key = parse_neb_key('neb://~foo/bar/baz/quix/');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'foo', 'volume name');
+    is($key->soft_volume, 1, 'soft volume name');
+}
+
+# URI w/ volume name and volume argument
+{
+    my $key = parse_neb_key('neb://foo/bar/baz/quix', 'boing');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'boing', 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb://foo/bar/baz/quix', '~boing');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'boing', 'volume name');
+    is($key->soft_volume, 1, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb://foo/bar/baz/quix', undef);
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'foo', 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+# URI w/ ~any volume name
+{
+    my $key = parse_neb_key('neb://~any/bar/baz/quix/');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+# URI w/ any volume argument
+{
+    my $key = parse_neb_key('neb://boing/bar/baz/quix/', 'any');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb://boing/bar/baz/quix/', '~any');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+# URI w/ soft volume name
+{
+    my $key = parse_neb_key('neb://~foo/bar/baz/quix/');
+
+    is($key->path, 'bar/baz/quix', 'path');
+    is($key->volume, 'foo', 'volume name');
+    is($key->soft_volume, 1, 'soft volume name');
+}
 
 # URI w/o volume name
 
 {
-    my ($volume, $path) = parse_neb_key('neb:/foo/bar/baz/quix');
-
-    is($path, 'foo/bar/baz/quix', 'path');
-    is($volume, undef, 'volume name');
-}
-
-{
-    my ($volume, $path) = parse_neb_key('neb:///foo/bar/baz/quix');
-
-    is($volume, undef, 'volume name');
-    is($path, 'foo/bar/baz/quix', 'path');
-}
-
-{
-    my ($volume, $path) = parse_neb_key('neb://///foo/bar/baz/quix');
-
-    is($path, 'foo/bar/baz/quix', 'path');
-    is($volume, undef, 'volume name');
+    my $key = parse_neb_key('neb:/foo/bar/baz/quix');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb:///foo/bar/baz/quix');
+
+    is($key->volume, undef, 'volume name');
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->soft_volume, undef, 'soft volume name');
+}
+
+{
+    my $key = parse_neb_key('neb://///foo/bar/baz/quix');
+
+    is($key->path, 'foo/bar/baz/quix', 'path');
+    is($key->volume, undef, 'volume name');
+    is($key->soft_volume, undef, 'soft volume name');
 }
 
@@ -166,5 +264,5 @@
 # URI w/o volume requires leading slash
 eval {
-    my ($volume, $path) = parse_neb_key('neb:foo/bar/baz/quix');
+    my $key = parse_neb_key('neb:foo/bar/baz/quix');
 };
 like( $@, qr/requires a leading slash/, "leading slash" );
