Skip to content

Commit

Permalink
Add sourceaddrs.ParseFinalSource
Browse files Browse the repository at this point in the history
We need to be able to decode string representations of final sources,
and the existing ParseSource function doesn't handle versioned registry
source addresses. This commit introduces ParseFileSource and supporting
functions for decoding final registry sources.
  • Loading branch information
alisdair committed Oct 27, 2023
1 parent aeb5117 commit d973687
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 0 deletions.
37 changes: 37 additions & 0 deletions sourceaddrs/source_final.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sourceaddrs
import (
"fmt"
"path"
"strings"
)

// FinalSource is a variant of [Source] that always refers to a single
Expand All @@ -18,6 +19,42 @@ type FinalSource interface {
String() string
}

// ParseFinalSource attempts to parse the given string as any one of the three
// supported final source address types, recognizing which type it belongs to
// based on the syntax differences between the address forms.
func ParseFinalSource(given string) (FinalSource, error) {
if strings.TrimSpace(given) != given {
return nil, fmt.Errorf("source address must not have leading or trailing spaces")
}
if len(given) == 0 {
return nil, fmt.Errorf("a valid source address is required")
}
switch {
case looksLikeLocalSource(given) || given == "." || given == "..":
ret, err := ParseLocalSource(given)
if err != nil {
return nil, fmt.Errorf("invalid local source address %q: %w", given, err)
}
return ret, nil
case looksLikeFinalRegistrySource(given):
ret, err := ParseFinalRegistrySource(given)
if err != nil {
return nil, fmt.Errorf("invalid module registry source address %q: %w", given, err)
}
return ret, nil
default:
// If it's neither a local source nor a final module registry source
// then we'll assume it's intended to be a remote source.
// (This parser will return a suitable error if the given string
// is not of any of the supported address types.)
ret, err := ParseRemoteSource(given)
if err != nil {
return nil, fmt.Errorf("invalid remote source address %q: %w", given, err)
}
return ret, nil
}
}

// FinalSourceFilename returns the base name (in the same sense as [path.Base])
// of the sub-path or local path portion of the given final source address.
//
Expand Down
75 changes: 75 additions & 0 deletions sourceaddrs/source_final_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,78 @@ func TestResolveRelativeFinalSource(t *testing.T) {
})
}
}

func TestParseFinalSource(t *testing.T) {
onePointOh := versions.MustParseVersion("1.0.0")

tests := []struct {
Addr string
Want FinalSource
WantErr string
}{
{
Addr: "./a/b",
Want: MustParseSource("./a/b").(FinalSource),
},
{
Addr: "git::https://github.com/hashicorp/go-slug.git//beep/boop",
Want: MustParseSource("git::https://github.com/hashicorp/go-slug.git//beep/boop").(FinalSource),
},
{
Addr: "example.com/foo/bar/[email protected]//beep",
Want: MustParseSource("example.com/foo/bar/baz//beep").(RegistrySource).Versioned(onePointOh),
},
{
Addr: "example.com/foo/bar/[email protected]",
Want: MustParseSource("example.com/foo/bar/baz").(RegistrySource).Versioned(onePointOh),
},
{
Addr: "./a/[email protected]",
Want: MustParseSource("./a/[email protected]").(FinalSource),
},
{
Addr: " ./a/b",
WantErr: "source address must not have leading or trailing spaces",
},
{
Addr: "",
WantErr: "a valid source address is required",
},
{
Addr: "example.com/foo/bar/[email protected]//beep",
WantErr: `invalid module registry source address "example.com/foo/bar/[email protected]//beep": invalid version: can't use wildcard for patch number; an exact version is required`,
},
}

for _, test := range tests {
t.Run(test.Addr, func(t *testing.T) {
got, gotErr := ParseFinalSource(test.Addr)

if test.WantErr != "" {
if gotErr == nil {
t.Fatalf("unexpected success\ngot result: %#v (%T)\nwant error: %s", got, got, test.WantErr)
}
if got, want := gotErr.Error(), test.WantErr; got != want {
t.Fatalf("wrong error\ngot error: %s\nwant error: %s", got, want)
}
return
}

if gotErr != nil {
t.Fatalf("unexpected error: %s", gotErr)
}

// Two addresses are equal if they have the same string representation
// and the same dynamic type.
gotStr := got.String()
wantStr := test.Want.String()
if gotStr != wantStr {
t.Errorf("wrong result\ngot: %s\nwant: %s", gotStr, wantStr)
}

if gotType, wantType := reflect.TypeOf(got), reflect.TypeOf(test.Want); gotType != wantType {
t.Errorf("wrong result type\ngot: %s\nwant: %s", gotType, wantType)
}
})
}
}
42 changes: 42 additions & 0 deletions sourceaddrs/source_registry_final.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package sourceaddrs

import (
"fmt"
"regexp"

"github.com/apparentlymart/go-versions/versions"
regaddr "github.com/hashicorp/terraform-registry-address"
)
Expand All @@ -24,9 +27,43 @@ type RegistrySourceFinal struct {
// string.
var _ FinalSource = RegistrySourceFinal{}

func looksLikeFinalRegistrySource(given string) bool {
var addr string
if matches := finalRegistrySourcePattern.FindStringSubmatch(given); len(matches) != 0 {
addr = matches[1]
if len(matches) == 5 {
addr = fmt.Sprintf("%s//%s", addr, matches[4])
}
}
return looksLikeRegistrySource(addr)
}

// finalSourceSigil implements FinalSource
func (s RegistrySourceFinal) finalSourceSigil() {}

// ParseFinalRegistrySource parses the given string as a final registry source
// address, or returns an error if it does not use the correct syntax for
// interpretation as a final registry source address.
func ParseFinalRegistrySource(given string) (RegistrySourceFinal, error) {
var addr, ver string
if matches := finalRegistrySourcePattern.FindStringSubmatch(given); len(matches) != 0 {
addr = matches[1]
ver = matches[2]
if len(matches) == 5 {
addr = fmt.Sprintf("%s//%s", addr, matches[4])
}
}
version, err := versions.ParseVersion(ver)
if err != nil {
return RegistrySourceFinal{}, fmt.Errorf("invalid version: %w", err)
}
regSrc, err := ParseRegistrySource(addr)
if err != nil {
return RegistrySourceFinal{}, fmt.Errorf("invalid registry source: %w", err)
}
return regSrc.Versioned(version), nil
}

// Unversioned returns the address of the registry package that this final
// address is a version of.
func (s RegistrySourceFinal) Unversioned() RegistrySource {
Expand Down Expand Up @@ -62,3 +99,8 @@ func (s RegistrySourceFinal) FinalSourceAddr(realSource RemoteSource) RemoteSour
// paths together, so we can just delegate to our unversioned equivalent.
return s.Unversioned().FinalSourceAddr(realSource)
}

// finalRegistrySourcePattern is a non-exhaustive regexp which looks only for
// the expected three components of a RegistrySourceFinal string encoding: the
// package address, version, and subpath. The subpath is optional.
var finalRegistrySourcePattern = regexp.MustCompile(`^(.+)@([^/]+)(//(.+))?$`)

0 comments on commit d973687

Please sign in to comment.