diff --git a/pkg/command/command.go b/pkg/command/command.go index 6e6efc2..618f023 100644 --- a/pkg/command/command.go +++ b/pkg/command/command.go @@ -16,14 +16,12 @@ var _ fmt.Stringer = (*Command)(nil) // by one or more slash-separated Segments of lowercase characters. // // [Command]: https://github.com/ucan-wg/spec#command -type Command struct { - segments []string -} +type Command string // New creates a validated command from the provided list of segment strings. // An error is returned if an invalid Command would be formed func New(segments ...string) Command { - return Command{segments: segments} + return Top().Join(segments...) } // Parse verifies that the provided string contains the required @@ -33,20 +31,20 @@ func New(segments ...string) Command { // [segment structure]: https://github.com/ucan-wg/spec#segment-structure func Parse(s string) (Command, error) { if !strings.HasPrefix(s, "/") { - return Command{}, ErrRequiresLeadingSlash + return "", ErrRequiresLeadingSlash } if len(s) > 1 && strings.HasSuffix(s, "/") { - return Command{}, ErrDisallowsTrailingSlash + return "", ErrDisallowsTrailingSlash } if s != strings.ToLower(s) { - return Command{}, ErrRequiresLowercase + return "", ErrRequiresLowercase } // The leading slash will result in the first element from strings.Split // being an empty string which is removed as strings.Join will ignore it. - return Command{strings.Split(s, "/")[1:]}, nil + return Command(s), nil } // MustParse is the same as Parse, but panic() if the parsing fail. @@ -58,14 +56,14 @@ func MustParse(s string) Command { return c } -// [Top] is the most powerful capability. +// Top is the most powerful capability. // // This function returns a Command that is a wildcard and therefore represents the // most powerful ability. As such, it should be handled with care and used sparingly. // // [Top]: https://github.com/ucan-wg/spec#-aka-top func Top() Command { - return New() + return Command(separator) } // IsValid returns true if the provided string is a valid UCAN command. @@ -77,17 +75,34 @@ func IsValid(s string) bool { // Join appends segments to the end of this command using the required // segment separator. func (c Command) Join(segments ...string) Command { - return Command{append(c.segments, segments...)} + size := 0 + for _, s := range segments { + size += len(s) + } + if size == 0 { + return c + } + buf := make([]byte, 0, len(c)+size+len(segments)) + buf = append(buf, []byte(c)...) + for _, s := range segments { + if s != "" { + if len(buf) > 1 { + buf = append(buf, separator...) + } + buf = append(buf, []byte(s)...) + } + } + return Command(buf) } // Segments returns the ordered segments that comprise the Command as a // slice of strings. func (c Command) Segments() []string { - return c.segments + return strings.Split(string(c), separator) } // String returns the composed representation the command. This is also // the required wire representation (before IPLD encoding occurs.) func (c Command) String() string { - return "/" + strings.Join(c.segments, "/") + return string(c) } diff --git a/pkg/command/command_test.go b/pkg/command/command_test.go index fc896df..ed7b86f 100644 --- a/pkg/command/command_test.go +++ b/pkg/command/command_test.go @@ -13,73 +13,66 @@ func TestTop(t *testing.T) { } func TestIsValidCommand(t *testing.T) { - t.Parallel() - t.Run("succeeds when", func(t *testing.T) { - t.Parallel() - for _, testcase := range validTestcases(t) { - testcase := testcase - t.Run(testcase.name, func(t *testing.T) { - t.Parallel() - require.True(t, command.IsValid(testcase.inp)) }) } }) t.Run("fails when", func(t *testing.T) { - t.Parallel() - for _, testcase := range invalidTestcases(t) { - testcase := testcase - t.Run(testcase.name, func(t *testing.T) { - t.Parallel() - require.False(t, command.IsValid(testcase.inp)) }) } }) } +func TestNew(t *testing.T) { + require.Equal(t, command.Top(), command.New()) + require.Equal(t, "/foo", command.New("foo").String()) + require.Equal(t, "/foo/bar", command.New("foo", "bar").String()) + require.Equal(t, "/foo/bar/baz", command.New("foo", "bar/baz").String()) +} + func TestParseCommand(t *testing.T) { - t.Parallel() - t.Run("succeeds when", func(t *testing.T) { - t.Parallel() - for _, testcase := range validTestcases(t) { - testcase := testcase - t.Run(testcase.name, func(t *testing.T) { - t.Parallel() - cmd, err := command.Parse("/elem0/elem1/elem2") require.NoError(t, err) - require.NotNil(t, cmd) + require.NotEmpty(t, cmd) }) } }) t.Run("fails when", func(t *testing.T) { - t.Parallel() - for _, testcase := range invalidTestcases(t) { - testcase := testcase - t.Run(testcase.name, func(t *testing.T) { - t.Parallel() - cmd, err := command.Parse(testcase.inp) require.ErrorIs(t, err, testcase.err) - require.Equal(t, command.Command{}, cmd) + require.Zero(t, cmd) }) } }) } +func TestEquality(t *testing.T) { + require.True(t, command.MustParse("/foo/bar/baz") == command.MustParse("/foo/bar/baz")) + require.False(t, command.MustParse("/foo/bar/baz") == command.MustParse("/foo/bar/bazz")) + require.False(t, command.MustParse("/foo/bar") == command.MustParse("/foo/bar/baz")) +} + +func TestJoin(t *testing.T) { + require.Equal(t, "/foo", command.Top().Join("foo").String()) + require.Equal(t, "/foo/bar", command.Top().Join("foo/bar").String()) + require.Equal(t, "/foo/bar", command.Top().Join("foo", "bar").String()) + require.Equal(t, "/faz/boz/foo/bar", command.MustParse("/faz/boz").Join("foo/bar").String()) + require.Equal(t, "/faz/boz/foo/bar", command.MustParse("/faz/boz").Join("foo", "bar").String()) +} + type testcase struct { name string inp string