From 5ea69812ced19c6b9bae58bf5a0b1b69c17036c9 Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Fri, 27 Oct 2023 12:40:36 -0400 Subject: [PATCH] Add sourceaddrs.ParseFinalSource 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. --- sourceaddrs/source_final.go | 37 +++++++++++++ sourceaddrs/source_final_test.go | 83 ++++++++++++++++++++++++++++ sourceaddrs/source_registry_final.go | 42 ++++++++++++++ 3 files changed, 162 insertions(+) diff --git a/sourceaddrs/source_final.go b/sourceaddrs/source_final.go index ad85942..0487ac9 100644 --- a/sourceaddrs/source_final.go +++ b/sourceaddrs/source_final.go @@ -3,6 +3,7 @@ package sourceaddrs import ( "fmt" "path" + "strings" ) // FinalSource is a variant of [Source] that always refers to a single @@ -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. // diff --git a/sourceaddrs/source_final_test.go b/sourceaddrs/source_final_test.go index b2250a4..f97154d 100644 --- a/sourceaddrs/source_final_test.go +++ b/sourceaddrs/source_final_test.go @@ -106,3 +106,86 @@ 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: "git::https://github.com/hashicorp/go-slug.git//beep@1.2.3/boop", + Want: MustParseSource("git::https://github.com/hashicorp/go-slug.git//beep@1.2.3/boop").(FinalSource), + }, + { + Addr: "example.com/foo/bar/baz@1.0.0//beep", + Want: MustParseSource("example.com/foo/bar/baz//beep").(RegistrySource).Versioned(onePointOh), + }, + { + Addr: "example.com/foo/bar/baz@1.0.0", + Want: MustParseSource("example.com/foo/bar/baz").(RegistrySource).Versioned(onePointOh), + }, + { + Addr: "gitlab.com/hashicorp/go-slug/bleep@1.0.0", + Want: MustParseSource("gitlab.com/hashicorp/go-slug/bleep").(RegistrySource).Versioned(onePointOh), + }, + { + Addr: "./a/b@1.0.0", + Want: MustParseSource("./a/b@1.0.0").(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/baz@1.0.x//beep", + WantErr: `invalid module registry source address "example.com/foo/bar/baz@1.0.x//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) + } + }) + } +} diff --git a/sourceaddrs/source_registry_final.go b/sourceaddrs/source_registry_final.go index 7ed9338..ad4e876 100644 --- a/sourceaddrs/source_registry_final.go +++ b/sourceaddrs/source_registry_final.go @@ -1,6 +1,9 @@ package sourceaddrs import ( + "fmt" + "regexp" + "github.com/apparentlymart/go-versions/versions" regaddr "github.com/hashicorp/terraform-registry-address" ) @@ -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 { @@ -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(`^(.+)@([^/]+)(//(.+))?$`)