Merge pull request #39 from ucan-wg/command-as-string

command: make the type a string, for easier equality test
This commit is contained in:
Michael Muré
2024-10-09 18:30:45 +02:00
committed by GitHub
2 changed files with 51 additions and 43 deletions

View File

@@ -16,14 +16,12 @@ var _ fmt.Stringer = (*Command)(nil)
// by one or more slash-separated Segments of lowercase characters. // by one or more slash-separated Segments of lowercase characters.
// //
// [Command]: https://github.com/ucan-wg/spec#command // [Command]: https://github.com/ucan-wg/spec#command
type Command struct { type Command string
segments []string
}
// New creates a validated command from the provided list of segment strings. // New creates a validated command from the provided list of segment strings.
// An error is returned if an invalid Command would be formed // An error is returned if an invalid Command would be formed
func New(segments ...string) Command { func New(segments ...string) Command {
return Command{segments: segments} return Top().Join(segments...)
} }
// Parse verifies that the provided string contains the required // 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 // [segment structure]: https://github.com/ucan-wg/spec#segment-structure
func Parse(s string) (Command, error) { func Parse(s string) (Command, error) {
if !strings.HasPrefix(s, "/") { if !strings.HasPrefix(s, "/") {
return Command{}, ErrRequiresLeadingSlash return "", ErrRequiresLeadingSlash
} }
if len(s) > 1 && strings.HasSuffix(s, "/") { if len(s) > 1 && strings.HasSuffix(s, "/") {
return Command{}, ErrDisallowsTrailingSlash return "", ErrDisallowsTrailingSlash
} }
if s != strings.ToLower(s) { if s != strings.ToLower(s) {
return Command{}, ErrRequiresLowercase return "", ErrRequiresLowercase
} }
// The leading slash will result in the first element from strings.Split // 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. // 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. // MustParse is the same as Parse, but panic() if the parsing fail.
@@ -58,14 +56,14 @@ func MustParse(s string) Command {
return c 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 // 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. // most powerful ability. As such, it should be handled with care and used sparingly.
// //
// [Top]: https://github.com/ucan-wg/spec#-aka-top // [Top]: https://github.com/ucan-wg/spec#-aka-top
func Top() Command { func Top() Command {
return New() return Command(separator)
} }
// IsValid returns true if the provided string is a valid UCAN command. // 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 // Join appends segments to the end of this command using the required
// segment separator. // segment separator.
func (c Command) Join(segments ...string) Command { 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 // Segments returns the ordered segments that comprise the Command as a
// slice of strings. // slice of strings.
func (c Command) Segments() []string { func (c Command) Segments() []string {
return c.segments return strings.Split(string(c), separator)
} }
// String returns the composed representation the command. This is also // String returns the composed representation the command. This is also
// the required wire representation (before IPLD encoding occurs.) // the required wire representation (before IPLD encoding occurs.)
func (c Command) String() string { func (c Command) String() string {
return "/" + strings.Join(c.segments, "/") return string(c)
} }

View File

@@ -13,73 +13,66 @@ func TestTop(t *testing.T) {
} }
func TestIsValidCommand(t *testing.T) { func TestIsValidCommand(t *testing.T) {
t.Parallel()
t.Run("succeeds when", func(t *testing.T) { t.Run("succeeds when", func(t *testing.T) {
t.Parallel()
for _, testcase := range validTestcases(t) { for _, testcase := range validTestcases(t) {
testcase := testcase
t.Run(testcase.name, func(t *testing.T) { t.Run(testcase.name, func(t *testing.T) {
t.Parallel()
require.True(t, command.IsValid(testcase.inp)) require.True(t, command.IsValid(testcase.inp))
}) })
} }
}) })
t.Run("fails when", func(t *testing.T) { t.Run("fails when", func(t *testing.T) {
t.Parallel()
for _, testcase := range invalidTestcases(t) { for _, testcase := range invalidTestcases(t) {
testcase := testcase
t.Run(testcase.name, func(t *testing.T) { t.Run(testcase.name, func(t *testing.T) {
t.Parallel()
require.False(t, command.IsValid(testcase.inp)) 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) { func TestParseCommand(t *testing.T) {
t.Parallel()
t.Run("succeeds when", func(t *testing.T) { t.Run("succeeds when", func(t *testing.T) {
t.Parallel()
for _, testcase := range validTestcases(t) { for _, testcase := range validTestcases(t) {
testcase := testcase
t.Run(testcase.name, func(t *testing.T) { t.Run(testcase.name, func(t *testing.T) {
t.Parallel()
cmd, err := command.Parse("/elem0/elem1/elem2") cmd, err := command.Parse("/elem0/elem1/elem2")
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, cmd) require.NotEmpty(t, cmd)
}) })
} }
}) })
t.Run("fails when", func(t *testing.T) { t.Run("fails when", func(t *testing.T) {
t.Parallel()
for _, testcase := range invalidTestcases(t) { for _, testcase := range invalidTestcases(t) {
testcase := testcase
t.Run(testcase.name, func(t *testing.T) { t.Run(testcase.name, func(t *testing.T) {
t.Parallel()
cmd, err := command.Parse(testcase.inp) cmd, err := command.Parse(testcase.inp)
require.ErrorIs(t, err, testcase.err) 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 { type testcase struct {
name string name string
inp string inp string