Skip to content

Commit

Permalink
path: Introduce package with initial tftypes.AttributePath abstraction
Browse files Browse the repository at this point in the history
Reference: #81
Reference: #161
Reference: #215
Reference: #365

This introduces a native abstraction over terraform-plugin-go's `tftypes.AttributePath`, allowing the framework to own the implementation details and extend the functionality further. Provider developers will be closer to removing a direct dependency on terraform-plugin-go. This is a major breaking change, however it is necessary before 1.0 to prevent compatibility issues in the future.

This functionality is only intended to replace `tftypes.AttributePath` usage and not mess with the `tfsdk.Config`, `tfsdk.Plan`, and `tfsdk.State` type `tftypes.Value` data storage fields or their underlying data manipulation logic. This does leave the framework in an awkward half-state until those are further refactored (likely towards native `attr.Value`), but is done to try and reduce the review complexity of this initial migration. Additional followup changes will introduce the concept of path expressions, which will allow provider developers to match against multiple paths or reference parent paths in schema-based plan modifier and validation functionality.

To prevent import cycles between `attr` and `diag` packages due changing the `attr.TypeWithValidate` type `Validate` method signature from `Validate(context.Context, tftypes.Value, *tftypes.AttributePath) diag.Diagnostics` to `Validate(context.Context, tftypes.Value, path.Path) diag.Diagnostics`, the `TypeWithValidation` interface is moved a new `xattr` package underneath `attr` with Go and website documentation to direct provider developers to the other package. This will also solve a prior issue with trying to implement `TypeWithModifyPlan` support.

Naming and location of anything in this initial implementation can be adjusted as necessary.

Provider developers can migrate to the new path handling by replacing:

```go
tftypes.NewAttributePath().WithAttributeName("example")
```

With the equivalent:

```go
path.RootPath("example")
```

Then using the `(Path).At*` methods to extend the path definition instead of `(*tftypes.AttributePath).With*` methods:

| Current                  | New             |
| ------------------------ | --------------- |
| `WithAttributeName()`    | `AtName()`      |
| `WithElementKeyInt()`    | `AtListIndex()` |
| `WithElementKeyString()` | `AtMapKey()`    |
| `WithElementKeyValue()`  | `AtSetValue()`  |
  • Loading branch information
bflad committed Jun 22, 2022
1 parent 6e01dca commit ed4c5db
Show file tree
Hide file tree
Showing 139 changed files with 3,997 additions and 1,281 deletions.
4 changes: 4 additions & 0 deletions attr/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Package attr contains type and value interfaces for core framework and
// provider-defined data types. The underlying xattr package contains
// additional interfaces for advanced type functionality.
package attr
16 changes: 3 additions & 13 deletions attr/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package attr
import (
"context"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

// Type defines an interface for describing a kind of attribute. Types are
// collections of constraints and behaviors such that they can be reused on
// multiple attributes easily.
//
// Refer also to the xattr package, which contains additional extensions for
// Type, such as validation.
type Type interface {
// TerraformType returns the tftypes.Type that should be used to
// represent this type. This constrains what user input will be
Expand Down Expand Up @@ -74,18 +76,6 @@ type TypeWithElementTypes interface {
ElementTypes() []Type
}

// TypeWithValidate extends the Type interface to include a Validate method,
// used to bundle consistent validation logic with the Type.
type TypeWithValidate interface {
Type

// Validate returns any warnings or errors about the value that is
// being used to populate the Type. It is generally used to check the
// data format and ensure that it complies with the requirements of the
// Type.
Validate(context.Context, tftypes.Value, *tftypes.AttributePath) diag.Diagnostics
}

// TypeWithPlaintextDescription extends the Type interface to include a
// Description method, used to bundle extra information to include in attribute
// descriptions with the Type. It expects the description to be written as
Expand Down
3 changes: 3 additions & 0 deletions attr/xattr/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package xattr contains additional interfaces for attr types. This package
// is separate from the core attr package to prevent import cycles.
package xattr
22 changes: 22 additions & 0 deletions attr/xattr/type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package xattr

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

// TypeWithValidate extends the attr.Type interface to include a Validate
// method, used to bundle consistent validation logic with the Type.
type TypeWithValidate interface {
attr.Type

// Validate returns any warnings or errors about the value that is
// being used to populate the Type. It is generally used to check the
// data format and ensure that it complies with the requirements of the
// Type.
Validate(context.Context, tftypes.Value, path.Path) diag.Diagnostics
}
4 changes: 2 additions & 2 deletions diag/attribute_error_diagnostic.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package diag

import (
"github.com/hashicorp/terraform-plugin-go/tftypes"
"github.com/hashicorp/terraform-plugin-framework/path"
)

// NewAttributeErrorDiagnostic returns a new error severity diagnostic with the given summary, detail, and path.
func NewAttributeErrorDiagnostic(path *tftypes.AttributePath, summary string, detail string) DiagnosticWithPath {
func NewAttributeErrorDiagnostic(path path.Path, summary string, detail string) DiagnosticWithPath {
return withPath{
Diagnostic: NewErrorDiagnostic(summary, detail),
path: path,
Expand Down
4 changes: 2 additions & 2 deletions diag/attribute_warning_diagnostic.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package diag

import (
"github.com/hashicorp/terraform-plugin-go/tftypes"
"github.com/hashicorp/terraform-plugin-framework/path"
)

// NewAttributeWarningDiagnostic returns a new warning severity diagnostic with the given summary, detail, and path.
func NewAttributeWarningDiagnostic(path *tftypes.AttributePath, summary string, detail string) DiagnosticWithPath {
func NewAttributeWarningDiagnostic(path path.Path, summary string, detail string) DiagnosticWithPath {
return withPath{
Diagnostic: NewWarningDiagnostic(summary, detail),
path: path,
Expand Down
6 changes: 2 additions & 4 deletions diag/diagnostic.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package diag

import (
"github.com/hashicorp/terraform-plugin-go/tftypes"
)
import "github.com/hashicorp/terraform-plugin-framework/path"

// Diagnostic is an interface for providing enhanced feedback.
//
Expand Down Expand Up @@ -48,5 +46,5 @@ type DiagnosticWithPath interface {
//
// If present, this enables the display of source configuration context for
// supporting implementations such as Terraform CLI commands.
Path() *tftypes.AttributePath
Path() path.Path
}
6 changes: 3 additions & 3 deletions diag/diagnostics.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package diag

import (
"github.com/hashicorp/terraform-plugin-go/tftypes"
"github.com/hashicorp/terraform-plugin-framework/path"
)

// Diagnostics represents a collection of diagnostics.
Expand All @@ -11,12 +11,12 @@ import (
type Diagnostics []Diagnostic

// AddAttributeError adds a generic attribute error diagnostic to the collection.
func (diags *Diagnostics) AddAttributeError(path *tftypes.AttributePath, summary string, detail string) {
func (diags *Diagnostics) AddAttributeError(path path.Path, summary string, detail string) {
diags.Append(NewAttributeErrorDiagnostic(path, summary, detail))
}

// AddAttributeWarning adds a generic attribute warning diagnostic to the collection.
func (diags *Diagnostics) AddAttributeWarning(path *tftypes.AttributePath, summary string, detail string) {
func (diags *Diagnostics) AddAttributeWarning(path path.Path, summary string, detail string) {
diags.Append(NewAttributeWarningDiagnostic(path, summary, detail))
}

Expand Down
96 changes: 48 additions & 48 deletions diag/diagnostics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,53 @@ import (

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-go/tftypes"
"github.com/hashicorp/terraform-plugin-framework/path"
)

func TestDiagnosticsAddAttributeError(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
diags diag.Diagnostics
path *tftypes.AttributePath
path path.Path
summary string
detail string
expected diag.Diagnostics
}{
"nil-add": {
diags: nil,
path: tftypes.NewAttributePath().WithAttributeName("test"),
path: path.RootPath("test"),
summary: "one summary",
detail: "one detail",
expected: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "one summary", "one detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("test"), "one summary", "one detail"),
},
},
"add": {
diags: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "two summary", "two detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("test"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("test"), "two summary", "two detail"),
},
path: tftypes.NewAttributePath().WithAttributeName("test"),
path: path.RootPath("test"),
summary: "three summary",
detail: "three detail",
expected: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "two summary", "two detail"),
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "three summary", "three detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("test"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("test"), "two summary", "two detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("test"), "three summary", "three detail"),
},
},
"duplicate": {
diags: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "two summary", "two detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("test"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("test"), "two summary", "two detail"),
},
path: tftypes.NewAttributePath().WithAttributeName("test"),
path: path.RootPath("test"),
summary: "one summary",
detail: "one detail",
expected: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "two summary", "two detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("test"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("test"), "two summary", "two detail"),
},
},
}
Expand All @@ -75,45 +75,45 @@ func TestDiagnosticsAddAttributeWarning(t *testing.T) {

testCases := map[string]struct {
diags diag.Diagnostics
path *tftypes.AttributePath
path path.Path
summary string
detail string
expected diag.Diagnostics
}{
"nil-add": {
diags: nil,
path: tftypes.NewAttributePath().WithAttributeName("test"),
path: path.RootPath("test"),
summary: "one summary",
detail: "one detail",
expected: diag.Diagnostics{
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("test"), "one summary", "one detail"),
},
},
"add": {
diags: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "two summary", "two detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("test"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("test"), "two summary", "two detail"),
},
path: tftypes.NewAttributePath().WithAttributeName("test"),
path: path.RootPath("test"),
summary: "three summary",
detail: "three detail",
expected: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "two summary", "two detail"),
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "three summary", "three detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("test"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("test"), "two summary", "two detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("test"), "three summary", "three detail"),
},
},
"duplicate": {
diags: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "two summary", "two detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("test"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("test"), "two summary", "two detail"),
},
path: tftypes.NewAttributePath().WithAttributeName("test"),
path: path.RootPath("test"),
summary: "two summary",
detail: "two detail",
expected: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("test"), "two summary", "two detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("test"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("test"), "two summary", "two detail"),
},
},
}
Expand Down Expand Up @@ -285,16 +285,16 @@ func TestDiagnosticsAppend(t *testing.T) {
},
"append-less-specific": {
diags: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("error"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("warning"), "two summary", "two detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("error"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("warning"), "two summary", "two detail"),
},
in: diag.Diagnostics{
diag.NewErrorDiagnostic("one summary", "one detail"),
diag.NewWarningDiagnostic("two summary", "two detail"),
},
expected: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("error"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("warning"), "two summary", "two detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("error"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("warning"), "two summary", "two detail"),
diag.NewErrorDiagnostic("one summary", "one detail"),
diag.NewWarningDiagnostic("two summary", "two detail"),
},
Expand All @@ -305,14 +305,14 @@ func TestDiagnosticsAppend(t *testing.T) {
diag.NewWarningDiagnostic("two summary", "two detail"),
},
in: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("error"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("warning"), "two summary", "two detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("error"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("warning"), "two summary", "two detail"),
},
expected: diag.Diagnostics{
diag.NewErrorDiagnostic("one summary", "one detail"),
diag.NewWarningDiagnostic("two summary", "two detail"),
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("error"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("warning"), "two summary", "two detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("error"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("warning"), "two summary", "two detail"),
},
},
"empty-diagnostics": {
Expand Down Expand Up @@ -388,10 +388,10 @@ func TestDiagnosticsContains(t *testing.T) {
},
"matching-attribute-path": {
diags: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("error"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("warning"), "two summary", "two detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("error"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("warning"), "two summary", "two detail"),
},
in: diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("warning"), "two summary", "two detail"),
in: diag.NewAttributeWarningDiagnostic(path.RootPath("warning"), "two summary", "two detail"),
expected: true,
},
"nil-diagnostics": {
Expand All @@ -409,10 +409,10 @@ func TestDiagnosticsContains(t *testing.T) {
},
"different-attribute-path": {
diags: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("error"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("warning"), "two summary", "two detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("error"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("warning"), "two summary", "two detail"),
},
in: diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("different"), "two summary", "two detail"),
in: diag.NewAttributeWarningDiagnostic(path.RootPath("different"), "two summary", "two detail"),
expected: false,
},
"different-detail": {
Expand Down Expand Up @@ -441,8 +441,8 @@ func TestDiagnosticsContains(t *testing.T) {
},
"different-type-less-specific": {
diags: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("error"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("warning"), "two summary", "two detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("error"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("warning"), "two summary", "two detail"),
},
in: diag.NewWarningDiagnostic("two summary", "two detail"),
expected: false,
Expand All @@ -452,7 +452,7 @@ func TestDiagnosticsContains(t *testing.T) {
diag.NewErrorDiagnostic("one summary", "one detail"),
diag.NewWarningDiagnostic("two summary", "two detail"),
},
in: diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("warning"), "two summary", "two detail"),
in: diag.NewAttributeWarningDiagnostic(path.RootPath("warning"), "two summary", "two detail"),
expected: false,
},
}
Expand Down Expand Up @@ -487,8 +487,8 @@ func TestDiagnosticsHasError(t *testing.T) {
},
"matching-attribute-path": {
diags: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("error"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("warning"), "two summary", "two detail"),
diag.NewAttributeErrorDiagnostic(path.RootPath("error"), "one summary", "one detail"),
diag.NewAttributeWarningDiagnostic(path.RootPath("warning"), "two summary", "two detail"),
},
expected: true,
},
Expand Down
8 changes: 4 additions & 4 deletions diag/with_path.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package diag

import (
"github.com/hashicorp/terraform-plugin-go/tftypes"
"github.com/hashicorp/terraform-plugin-framework/path"
)

var _ DiagnosticWithPath = withPath{}
Expand All @@ -10,7 +10,7 @@ var _ DiagnosticWithPath = withPath{}
type withPath struct {
Diagnostic

path *tftypes.AttributePath
path path.Path
}

// Equal returns true if the other diagnostic is wholly equivalent.
Expand All @@ -33,12 +33,12 @@ func (d withPath) Equal(other Diagnostic) bool {
}

// Path returns the diagnostic path.
func (d withPath) Path() *tftypes.AttributePath {
func (d withPath) Path() path.Path {
return d.path
}

// WithPath wraps a diagnostic with path information or overwrites the path.
func WithPath(path *tftypes.AttributePath, d Diagnostic) DiagnosticWithPath {
func WithPath(path path.Path, d Diagnostic) DiagnosticWithPath {
wp, ok := d.(withPath)

if !ok {
Expand Down
Loading

0 comments on commit ed4c5db

Please sign in to comment.