feat: reorganize packages
This commit is contained in:
93
pkg/command/command.go
Normal file
93
pkg/command/command.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const separator = "/"
|
||||
|
||||
var _ fmt.Stringer = (*Command)(nil)
|
||||
|
||||
// Command is a concrete messages (a "verb") that MUST be unambiguously
|
||||
// interpretable by the Subject of a UCAN.
|
||||
//
|
||||
// A [Command] is composed of a leading slash which is optionally followed
|
||||
// by one or more slash-separated Segments of lowercase characters.
|
||||
//
|
||||
// [Command]: https://github.com/ucan-wg/spec#command
|
||||
type Command struct {
|
||||
segments []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}
|
||||
}
|
||||
|
||||
// Parse verifies that the provided string contains the required
|
||||
// [segment structure] and, if valid, returns the resulting
|
||||
// Command.
|
||||
//
|
||||
// [segment structure]: https://github.com/ucan-wg/spec#segment-structure
|
||||
func Parse(s string) (Command, error) {
|
||||
if !strings.HasPrefix(s, "/") {
|
||||
return Command{}, ErrRequiresLeadingSlash
|
||||
}
|
||||
|
||||
if len(s) > 1 && strings.HasSuffix(s, "/") {
|
||||
return Command{}, ErrDisallowsTrailingSlash
|
||||
}
|
||||
|
||||
if s != strings.ToLower(s) {
|
||||
return Command{}, 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
|
||||
}
|
||||
|
||||
// MustParse is the same as Parse, but panic() if the parsing fail.
|
||||
func MustParse(s string) Command {
|
||||
c, err := Parse(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// [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()
|
||||
}
|
||||
|
||||
// IsValid returns true if the provided string is a valid UCAN command.
|
||||
func IsValid(s string) bool {
|
||||
_, err := Parse(s)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// 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...)}
|
||||
}
|
||||
|
||||
// Segments returns the ordered segments that comprise the Command as a
|
||||
// slice of strings.
|
||||
func (c Command) Segments() []string {
|
||||
return c.segments
|
||||
}
|
||||
|
||||
// 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, "/")
|
||||
}
|
||||
21
pkg/command/command_errors.go
Normal file
21
pkg/command/command_errors.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package command
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ErrRequiresLeadingSlash is returned when a parsing a string that
|
||||
// doesn't start with a [leading slash character].
|
||||
//
|
||||
// [leading slash character]: https://github.com/ucan-wg/spec#segment-structure
|
||||
var ErrRequiresLeadingSlash = fmt.Errorf("a command requires a leading slash character")
|
||||
|
||||
// ErrDisallowsTrailingSlash is returned when parsing a string that [ends
|
||||
// with a trailing slash character].
|
||||
//
|
||||
// [ends with a trailing slash character]: https://github.com/ucan-wg/spec#segment-structure
|
||||
var ErrDisallowsTrailingSlash = fmt.Errorf("a command must not include a trailing slash")
|
||||
|
||||
// ErrRequiresLowercase is returned if a Command contains, or would contain,
|
||||
// [uppercase unicode characters].
|
||||
//
|
||||
// [uppercase unicode characters]: https://github.com/ucan-wg/spec#segment-structure
|
||||
var ErrRequiresLowercase = fmt.Errorf("UCAN path segments must must not contain upper-case characters")
|
||||
144
pkg/command/command_test.go
Normal file
144
pkg/command/command_test.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package command_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||
)
|
||||
|
||||
func TestTop(t *testing.T) {
|
||||
require.Equal(t, "/", command.Top().String())
|
||||
}
|
||||
|
||||
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 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)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type testcase struct {
|
||||
name string
|
||||
inp string
|
||||
}
|
||||
|
||||
func validTestcases(t *testing.T) []testcase {
|
||||
t.Helper()
|
||||
|
||||
cmds := []string{
|
||||
"/",
|
||||
"/crud",
|
||||
"/crud/create",
|
||||
"/stack/pop",
|
||||
"/crypto/sign",
|
||||
"/foo/bar/baz/qux/quux",
|
||||
"/ほげ/ふが",
|
||||
}
|
||||
|
||||
testcases := make([]testcase, len(cmds))
|
||||
|
||||
for i, inp := range cmds {
|
||||
testcases[i] = testcase{
|
||||
name: "Command is " + inp,
|
||||
inp: inp,
|
||||
}
|
||||
}
|
||||
|
||||
return testcases
|
||||
}
|
||||
|
||||
type errorTestcase struct {
|
||||
testcase
|
||||
err error
|
||||
}
|
||||
|
||||
func invalidTestcases(t *testing.T) []errorTestcase {
|
||||
t.Helper()
|
||||
|
||||
return []errorTestcase{
|
||||
{
|
||||
testcase: testcase{
|
||||
name: "leading slash is missing",
|
||||
inp: "elem0/elem1/elem2",
|
||||
},
|
||||
err: command.ErrRequiresLeadingSlash,
|
||||
},
|
||||
{
|
||||
testcase: testcase{
|
||||
name: "trailing slash is present",
|
||||
inp: "/elem0/elem1/elem2/",
|
||||
},
|
||||
err: command.ErrDisallowsTrailingSlash,
|
||||
},
|
||||
{
|
||||
testcase: testcase{
|
||||
name: "uppercase character are present",
|
||||
inp: "/elem0/Elem1/elem2",
|
||||
},
|
||||
err: command.ErrRequiresLowercase,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user