Skip to content

Commit 560cc40

Browse files
authored
fix ValidateProviderAddress + Introduce Provider.Validate + Provider (un)marshaler interfaces (#104)
1 parent 24096b9 commit 560cc40

File tree

2 files changed

+227
-9
lines changed

2 files changed

+227
-9
lines changed

provider.go

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,59 @@ func (pt Provider) IsZero() bool {
128128
return pt == Provider{}
129129
}
130130

131+
// Validate returns error if the Provider representing "modern"
132+
// (Terraform 0.14+) address is not valid. Valid address implies
133+
// both valid namespace and a non-empty hostname.
134+
//
135+
// Validation makes assumptions equivalent to [ValidateProviderAddress].
136+
//
137+
// If you can guarantee [ValidateProviderAddress] was called
138+
// on the input and the [Provider] data was not mutated
139+
// you should not need to call this method.
140+
func (pt Provider) Validate() error {
141+
if pt.IsZero() {
142+
return &ParserError{
143+
Summary: "Empty provider address",
144+
Detail: "Expected address composed of hostname, provider namespace and name",
145+
}
146+
}
147+
148+
if pt.Hostname == "" {
149+
return &ParserError{
150+
Summary: "Unknown hostname",
151+
Detail: "Expected hostname in the provider address to be set",
152+
}
153+
}
154+
if pt.Namespace == "" {
155+
return &ParserError{
156+
Summary: "Unknown provider namespace",
157+
Detail: "Expected provider namespace to be set",
158+
}
159+
}
160+
if pt.Type == "" {
161+
return &ParserError{
162+
Summary: "Unknown provider type",
163+
Detail: "Expected provider type to be set",
164+
}
165+
}
166+
167+
if !pt.HasKnownNamespace() {
168+
return &ParserError{
169+
Summary: "Unknown provider namespace",
170+
Detail: `Expected FQN in the format "hostname/namespace/name"`,
171+
}
172+
}
173+
174+
if pt.IsLegacy() {
175+
return &ParserError{
176+
Summary: "Invalid legacy provider namespace",
177+
Detail: `Expected FQN in the format "hostname/namespace/name"`,
178+
}
179+
}
180+
181+
return nil
182+
}
183+
131184
// HasKnownNamespace returns true if the provider namespace is known
132185
// (also if it is legacy namespace)
133186
func (pt Provider) HasKnownNamespace() bool {
@@ -162,6 +215,31 @@ func (pt Provider) LessThan(other Provider) bool {
162215
}
163216
}
164217

218+
// MarshalText implements encoding.TextMarshaler interface.
219+
//
220+
// It encodes the [Provider] into an FQN, equivalent to [String]
221+
// or returns an error for an invalid [Provider].
222+
func (pt Provider) MarshalText() ([]byte, error) {
223+
err := pt.Validate()
224+
if err != nil {
225+
return nil, err
226+
}
227+
228+
return []byte(pt.String()), nil
229+
}
230+
231+
// UnmarshalText implements encoding.TextUnmarshaler interface.
232+
//
233+
// It decodes a valid provider address or returns an error
234+
// using [ParseProviderSource].
235+
//
236+
// [Validate] should be called on the decoded [Provider]
237+
// if modern-style Terraform 0.14+ addresses are expected.
238+
func (pt *Provider) UnmarshalText(text []byte) (err error) {
239+
*pt, err = ParseProviderSource(string(text))
240+
return
241+
}
242+
165243
// IsLegacy returns true if the provider is a legacy-style provider
166244
func (pt Provider) IsLegacy() bool {
167245
if pt.IsZero() {
@@ -182,9 +260,10 @@ func (pt Provider) Equals(other Provider) bool {
182260
// terraform-config-inspect.
183261
//
184262
// The following are valid source string formats:
185-
// name
186-
// namespace/name
187-
// hostname/namespace/name
263+
//
264+
// name
265+
// namespace/name
266+
// hostname/namespace/name
188267
//
189268
// "name"-only format is parsed as -/name (i.e. legacy namespace)
190269
// requiring further identification of the namespace via Registry API
@@ -296,7 +375,7 @@ func ParseProviderSource(str string) (Provider, error) {
296375

297376
// MustParseProviderSource is a wrapper around ParseProviderSource that panics if
298377
// it returns an error.
299-
func MustParseProviderSource(raw string) (Provider) {
378+
func MustParseProviderSource(raw string) Provider {
300379
p, err := ParseProviderSource(raw)
301380
if err != nil {
302381
panic(err)
@@ -332,7 +411,7 @@ func ValidateProviderAddress(raw string) error {
332411
}
333412
}
334413

335-
if !p.IsLegacy() {
414+
if p.IsLegacy() {
336415
return &ParserError{
337416
Summary: "Invalid legacy provider namespace",
338417
Detail: `Expected FQN in the format "hostname/namespace/name"`,

provider_test.go

Lines changed: 143 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package tfaddr
55

66
import (
7+
"encoding/json"
78
"fmt"
89
"log"
910
"testing"
@@ -247,6 +248,148 @@ func TestProviderIsLegacy(t *testing.T) {
247248
}
248249
}
249250

251+
func TestValidate(t *testing.T) {
252+
tests := []struct {
253+
Input Provider
254+
ExpectedErr bool
255+
}{
256+
{
257+
MustParseProviderSource("host.com/hashicorp/coffee"),
258+
false,
259+
},
260+
{
261+
Provider{},
262+
true,
263+
},
264+
{
265+
Provider{
266+
Type: "latte",
267+
},
268+
true,
269+
},
270+
{
271+
Provider{
272+
Type: "latte",
273+
Namespace: "coffeeshop",
274+
},
275+
true,
276+
},
277+
{
278+
Provider{
279+
Hostname: svchost.Hostname("registry.terraform.io"),
280+
},
281+
true,
282+
},
283+
{
284+
MustParseProviderSource("unknown-namespace"),
285+
true,
286+
},
287+
{
288+
MustParseProviderSource("-/legacy"),
289+
true,
290+
},
291+
}
292+
293+
for i, tc := range tests {
294+
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
295+
err := tc.Input.Validate()
296+
if err != nil {
297+
if tc.ExpectedErr {
298+
return
299+
}
300+
t.Fatalf("unexpected validation error: %s", err)
301+
}
302+
if tc.ExpectedErr {
303+
t.Fatal("expected validation error, none received")
304+
}
305+
})
306+
}
307+
}
308+
309+
func TestValidateProviderAddress(t *testing.T) {
310+
tests := []struct {
311+
RawInput string
312+
ExpectedErr bool
313+
}{
314+
{
315+
"host.com/hashicorp/coffee",
316+
false,
317+
},
318+
{
319+
"",
320+
true,
321+
},
322+
{
323+
"latte",
324+
true,
325+
},
326+
{
327+
"coffeeshop/latte",
328+
true,
329+
},
330+
{
331+
"registry.terraform.io//",
332+
true,
333+
},
334+
{
335+
"unknown-namespace",
336+
true,
337+
},
338+
{
339+
"-/legacy",
340+
true,
341+
},
342+
}
343+
344+
for i, tc := range tests {
345+
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
346+
err := ValidateProviderAddress(tc.RawInput)
347+
if err != nil {
348+
if tc.ExpectedErr {
349+
return
350+
}
351+
t.Fatalf("unexpected validation error for %q: %s", tc.RawInput, err)
352+
}
353+
if tc.ExpectedErr {
354+
t.Fatal("expected validation error, none received")
355+
}
356+
})
357+
}
358+
}
359+
360+
func TestProviderMarshalText(t *testing.T) {
361+
p := Provider{
362+
Hostname: svchost.Hostname("registry.terraform.io"),
363+
Namespace: "hashicorp",
364+
Type: "aws",
365+
}
366+
b, err := json.Marshal(p)
367+
if err != nil {
368+
t.Fatal(err)
369+
}
370+
expected := `"registry.terraform.io/hashicorp/aws"`
371+
if diff := cmp.Diff(expected, string(b)); diff != "" {
372+
t.Fatalf("marshaled text mismatch: %s", diff)
373+
}
374+
}
375+
376+
func TestProviderUnmarshalText(t *testing.T) {
377+
address := `"registry.terraform.io/hashicorp/aws"`
378+
var p Provider
379+
err := json.Unmarshal([]byte(address), &p)
380+
if err != nil {
381+
t.Fatal(err)
382+
}
383+
expectedProvider := Provider{
384+
Hostname: svchost.Hostname("registry.terraform.io"),
385+
Namespace: "hashicorp",
386+
Type: "aws",
387+
}
388+
if diff := cmp.Diff(expectedProvider, p); diff != "" {
389+
t.Fatalf("unmarshaled provider mismatch: %s", diff)
390+
}
391+
}
392+
250393
func ExampleParseProviderSource() {
251394
pAddr, err := ParseProviderSource("hashicorp/aws")
252395
if err != nil {
@@ -561,7 +704,3 @@ func TestProviderEquals(t *testing.T) {
561704
})
562705
}
563706
}
564-
565-
func TestValidateProviderAddress(t *testing.T) {
566-
t.Skip("TODO")
567-
}

0 commit comments

Comments
 (0)