Skip to content

Commit 7055f97

Browse files
authored
Create diag package and switch all applicable []tfprotov6.Diagnostic usage (#110)
Create a diag package with a native abstraction for diagnostics, represented as an interface so they can be discriminated between programmatically rather than acting only as practitioner-facing values. Switch all existing diagnostic usage to the new interface. Create new diagnostics helpers implementing the new interface.
1 parent dfae688 commit 7055f97

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+3820
-2592
lines changed

.changelog/110.txt

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
```release-note:breaking-change
2+
Most uses of `[]*tfprotov6.Diagnostic` have been replaced with a new `diag.Diagnostics` type. Please update your type signatures, and use one of the `diags.New*` helper functions instead of constructing `*tfprotov6.Diagnostic`s by hand.
3+
```
4+
5+
```release-note:feature
6+
Introduced first-class diagnostics (`diag` package).
7+
```

attr/type.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package attr
33
import (
44
"context"
55

6-
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
6+
"github.com/hashicorp/terraform-plugin-framework/diag"
77
"github.com/hashicorp/terraform-plugin-go/tftypes"
88
)
99

@@ -80,9 +80,7 @@ type TypeWithValidate interface {
8080
// being used to populate the Type. It is generally used to check the
8181
// data format and ensure that it complies with the requirements of the
8282
// Type.
83-
//
84-
// TODO: don't use tfprotov6.Diagnostic, use our type
85-
Validate(context.Context, tftypes.Value) []*tfprotov6.Diagnostic
83+
Validate(context.Context, tftypes.Value) diag.Diagnostics
8684
}
8785

8886
// TypeWithPlaintextDescription extends the Type interface to include a

diag/attribute_error_diagnostic.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package diag
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-go/tftypes"
5+
)
6+
7+
var _ DiagnosticWithPath = AttributeErrorDiagnostic{}
8+
9+
// AttributeErrorDiagnostic is a generic attribute diagnostic with error severity.
10+
type AttributeErrorDiagnostic struct {
11+
ErrorDiagnostic
12+
13+
path *tftypes.AttributePath
14+
}
15+
16+
// Equal returns true if the other diagnostic is wholly equivalent.
17+
func (d AttributeErrorDiagnostic) Equal(other Diagnostic) bool {
18+
aed, ok := other.(AttributeErrorDiagnostic)
19+
20+
if !ok {
21+
return false
22+
}
23+
24+
if !aed.Path().Equal(d.Path()) {
25+
return false
26+
}
27+
28+
return aed.ErrorDiagnostic.Equal(d.ErrorDiagnostic)
29+
}
30+
31+
// Path returns the diagnostic path.
32+
func (d AttributeErrorDiagnostic) Path() *tftypes.AttributePath {
33+
return d.path
34+
}
35+
36+
// NewAttributeErrorDiagnostic returns a new error severity diagnostic with the given summary, detail, and path.
37+
func NewAttributeErrorDiagnostic(path *tftypes.AttributePath, summary string, detail string) AttributeErrorDiagnostic {
38+
return AttributeErrorDiagnostic{
39+
ErrorDiagnostic: ErrorDiagnostic{
40+
detail: detail,
41+
summary: summary,
42+
},
43+
path: path,
44+
}
45+
}

diag/attribute_warning_diagnostic.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package diag
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-go/tftypes"
5+
)
6+
7+
var _ DiagnosticWithPath = AttributeWarningDiagnostic{}
8+
9+
// AttributeErrorDiagnostic is a generic attribute diagnostic with warning severity.
10+
type AttributeWarningDiagnostic struct {
11+
WarningDiagnostic
12+
13+
path *tftypes.AttributePath
14+
}
15+
16+
// Equal returns true if the other diagnostic is wholly equivalent.
17+
func (d AttributeWarningDiagnostic) Equal(other Diagnostic) bool {
18+
awd, ok := other.(AttributeWarningDiagnostic)
19+
20+
if !ok {
21+
return false
22+
}
23+
24+
if !awd.Path().Equal(d.Path()) {
25+
return false
26+
}
27+
28+
return awd.WarningDiagnostic.Equal(d.WarningDiagnostic)
29+
}
30+
31+
// Path returns the diagnostic path.
32+
func (d AttributeWarningDiagnostic) Path() *tftypes.AttributePath {
33+
return d.path
34+
}
35+
36+
// NewAttributeWarningDiagnostic returns a new warning severity diagnostic with the given summary, detail, and path.
37+
func NewAttributeWarningDiagnostic(path *tftypes.AttributePath, summary string, detail string) AttributeWarningDiagnostic {
38+
return AttributeWarningDiagnostic{
39+
WarningDiagnostic: WarningDiagnostic{
40+
detail: detail,
41+
summary: summary,
42+
},
43+
path: path,
44+
}
45+
}

diag/diagnostic.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package diag
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-go/tftypes"
5+
)
6+
7+
// Diagnostic is an interface for providing enhanced feedback.
8+
//
9+
// These are typically practitioner facing, however it is possible for
10+
// functionality, such as validation, to use these to change behaviors or
11+
// otherwise have these be manipulated or removed before being presented.
12+
//
13+
// See the ErrorDiagnostic and WarningDiagnostic concrete types for generic
14+
// implementations.
15+
type Diagnostic interface {
16+
// Severity returns the desired level of feedback for the diagnostic.
17+
Severity() Severity
18+
19+
// Summary is a short description for the diagnostic.
20+
//
21+
// Typically this is implemented as a title, such as "Invalid Resource Name",
22+
// or single line sentence.
23+
Summary() string
24+
25+
// Detail is a long description for the diagnostic.
26+
//
27+
// This should contain all relevant information about why the diagnostic
28+
// was generated and if applicable, ways to prevent the diagnostic. It
29+
// should generally be written and formatted for human consumption by
30+
// practitioners or provider developers.
31+
Detail() string
32+
33+
// Equal returns true if the other diagnostic is wholly equivalent.
34+
Equal(Diagnostic) bool
35+
}
36+
37+
// DiagnosticWithPath is a diagnostic associated with an attribute path.
38+
//
39+
// This attribute information is used to display contextual source configuration
40+
// to practitioners.
41+
type DiagnosticWithPath interface {
42+
Diagnostic
43+
44+
// Path points to a specific value within an aggregate value.
45+
//
46+
// If present, this enables the display of source configuration context for
47+
// supporting implementations such as Terraform CLI commands.
48+
Path() *tftypes.AttributePath
49+
}

diag/diagnostic_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package diag_test
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-framework/diag"
5+
)
6+
7+
var _ diag.Diagnostic = invalidSeverityDiagnostic{}
8+
9+
type invalidSeverityDiagnostic struct{}
10+
11+
func (d invalidSeverityDiagnostic) Detail() string {
12+
return "detail for invalid severity diagnostic"
13+
}
14+
15+
func (d invalidSeverityDiagnostic) Equal(other diag.Diagnostic) bool {
16+
isd, ok := other.(invalidSeverityDiagnostic)
17+
18+
if !ok {
19+
return false
20+
}
21+
22+
return isd.Summary() == d.Summary() && isd.Detail() == d.Detail() && isd.Severity() == d.Severity()
23+
}
24+
25+
func (d invalidSeverityDiagnostic) Severity() diag.Severity {
26+
return diag.SeverityInvalid
27+
}
28+
29+
func (d invalidSeverityDiagnostic) Summary() string {
30+
return "summary for invalid severity diagnostic"
31+
}

diag/diagnostics.go

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package diag
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
5+
"github.com/hashicorp/terraform-plugin-go/tftypes"
6+
)
7+
8+
// Diagnostics represents a collection of diagnostics.
9+
//
10+
// While this collection is ordered, the order is not guaranteed as reliable
11+
// or consistent.
12+
type Diagnostics []Diagnostic
13+
14+
// AddAttributeError adds a generic attribute error diagnostic to the collection.
15+
func (diags *Diagnostics) AddAttributeError(path *tftypes.AttributePath, summary string, detail string) {
16+
diags.Append(NewAttributeErrorDiagnostic(path, summary, detail))
17+
}
18+
19+
// AddAttributeWarning adds a generic attribute warning diagnostic to the collection.
20+
func (diags *Diagnostics) AddAttributeWarning(path *tftypes.AttributePath, summary string, detail string) {
21+
diags.Append(NewAttributeWarningDiagnostic(path, summary, detail))
22+
}
23+
24+
// AddError adds a generic error diagnostic to the collection.
25+
func (diags *Diagnostics) AddError(summary string, detail string) {
26+
diags.Append(NewErrorDiagnostic(summary, detail))
27+
}
28+
29+
// AddWarning adds a generic warning diagnostic to the collection.
30+
func (diags *Diagnostics) AddWarning(summary string, detail string) {
31+
diags.Append(NewWarningDiagnostic(summary, detail))
32+
}
33+
34+
// Append adds non-empty and non-duplicate diagnostics to the collection.
35+
func (diags *Diagnostics) Append(in ...Diagnostic) {
36+
for _, diag := range in {
37+
if diag == nil {
38+
continue
39+
}
40+
41+
if diags.Contains(diag) {
42+
continue
43+
}
44+
45+
if diags == nil {
46+
*diags = Diagnostics{diag}
47+
} else {
48+
*diags = append(*diags, diag)
49+
}
50+
}
51+
}
52+
53+
// Contains returns true if the collection contains an equal Diagnostic.
54+
func (diags Diagnostics) Contains(in Diagnostic) bool {
55+
for _, diag := range diags {
56+
if diag.Equal(in) {
57+
return true
58+
}
59+
}
60+
61+
return false
62+
}
63+
64+
// HasError returns true if the collection has an error severity Diagnostic.
65+
func (diags Diagnostics) HasError() bool {
66+
for _, diag := range diags {
67+
if diag.Severity() == SeverityError {
68+
return true
69+
}
70+
}
71+
72+
return false
73+
}
74+
75+
// ToTfprotov6Diagnostics converts the diagnostics into the tfprotov6 collection type.
76+
//
77+
// Usage of this method outside the framework is not supported nor considered
78+
// for backwards compatibility promises.
79+
func (diags Diagnostics) ToTfprotov6Diagnostics() []*tfprotov6.Diagnostic {
80+
var results []*tfprotov6.Diagnostic
81+
82+
for _, diag := range diags {
83+
tfprotov6Diagnostic := &tfprotov6.Diagnostic{
84+
Detail: diag.Detail(),
85+
Severity: diag.Severity().ToTfprotov6DiagnosticSeverity(),
86+
Summary: diag.Summary(),
87+
}
88+
89+
if diagWithPath, ok := diag.(DiagnosticWithPath); ok {
90+
tfprotov6Diagnostic.Attribute = diagWithPath.Path()
91+
}
92+
93+
results = append(results, tfprotov6Diagnostic)
94+
}
95+
96+
return results
97+
}

0 commit comments

Comments
 (0)