Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Missing NestingMode in GetSchema makes Terraform crash #326

Closed
abergmeier opened this issue May 13, 2022 · 7 comments · Fixed by #576
Closed

Missing NestingMode in GetSchema makes Terraform crash #326

abergmeier opened this issue May 13, 2022 · 7 comments · Fixed by #576
Labels
bug Something isn't working
Milestone

Comments

@abergmeier
Copy link

Module version

github.com/hashicorp/terraform-plugin-framework v0.8.0

Relevant provider source code

var (
	outputDockerAttributes = map[string]tfsdk.Attribute{}
	outputBlocks                             = map[string]tfsdk.Block{
		"docker": {
			Attributes: outputDockerAttributes,
			MaxItems:   1,
			PlanModifiers: tfsdk.AttributePlanModifiers{
				tfsdk.RequiresReplace(),
			},
		},
	}
)

func (t builtResourceType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
	return tfsdk.Schema{
		Blocks: map[string]tfsdk.Block{
			"output": {
				MinItems:            1,
				MaxItems:            1,
				Blocks:              outputBlocks,
				Description:         "Output destination. For entries of type gha, GitHub Action credentials are automatically added to attrs.",
				MarkdownDescription: "Output destination. For entries of `type` `gha`, GitHub Action credentials are automatically added to attrs.",
				PlanModifiers: tfsdk.AttributePlanModifiers{
					tfsdk.RequiresReplace(),
				},
			},
		},
	}
}

Debug Output

See https://gist.github.com/abergmeier/fe55ad498453c30c85dde63aca2aa54b

Expected Behavior

framework should have validation in place to not allow "invalid" Schemas.

Actual Behavior

Make Terraform crash.

Steps to Reproduce

terraform apply

References

@abergmeier abergmeier added the bug Something isn't working label May 13, 2022
@abergmeier
Copy link
Author

Introducing NestingMode: seems to fix the problem - so maybe validate whether NestingMode is unknown?

@abergmeier abergmeier changed the title GetSchema implementation crashes Terraform Missing NestingMode in GetSchema makes Terraform crash May 13, 2022
@bflad
Copy link
Contributor

bflad commented May 13, 2022

Hi @abergmeier 👋 Thank you for raising this and sorry you ran into trouble here.

Glad we have a lead on what caused the issue. We should also ensure there is a bug report raised in the Terraform CLI issue tracker, because they should likely raise a proper error instead of crashing. I'm guessing this is on the latest Terraform CLI version?

In terms of resolution here, we likely cannot raise a compiler error without making the schema code more complicated, however we do have plans in #113 to add provider unit testing functionality, similar to how you could get unit testing failures with terraform-plugin-sdk.

Do you also think the documentation could be improved anywhere to help avoid this? Your perspective would be super helpful. Thanks!

@abergmeier
Copy link
Author

In terms of resolution here, we likely cannot raise a compiler error without making the schema code more complicated

Well the next best thing could be for the function calling GetSchema to recursively validate NestingMode.

Do you also think the documentation could be improved anywhere to help avoid this?

NestingMode has next to no docstring to begin with - so maybe would be good to describe the rules for when NestingMode is necessary.

Also maybe a better design would be for 0 to actually mean "Auto" and by default fallback to List, when NestingMode is needed?

In the end IMO it comes down how accessible you want the framework to be.

@bflad
Copy link
Contributor

bflad commented May 17, 2022

In the code that converts from the framework's schema to the protocol-level schema, there is already an error/validation check for NestedMode not being list or set:

nm := b.NestingMode
switch nm {
case tfsdk.BlockNestingModeList:
schemaNestedBlock.Nesting = tfprotov6.SchemaNestedBlockNestingModeList
case tfsdk.BlockNestingModeSet:
schemaNestedBlock.Nesting = tfprotov6.SchemaNestedBlockNestingModeSet
default:
return nil, path.NewErrorf("unrecognized nesting mode %v", nm)
}

This should bubble up the error to here:

proto6, err := Block(ctx, name, tftypes.NewAttributePath().WithAttributeName(name), block)
if err != nil {
return nil, err
}

Which eventually winds up as an error diagnostic in the GetProviderSchema RPC response:

protov6.ResourceSchemas[resourceType], err = Schema(ctx, resourceSchema)
if err != nil {
protov6.Diagnostics = append(protov6.Diagnostics, &tfprotov6.Diagnostic{
Severity: tfprotov6.DiagnosticSeverityError,
Summary: "Error converting resource schema",
Detail: "The schema for the resource \"" + resourceType + "\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\n" + err.Error(),
})
return protov6
}

It doesn't appear that Terraform CLI is properly checking for error diagnostics to return early when it receives an error diagnostic:

https://github.com/hashicorp/terraform/blob/26ead07b68c106356b8d772fbeb36efd27949862/internal/plugin6/grpc_provider.go#L139-L145

So outside that, since Terraform CLI isn't properly handling nil on its side when it tries to convert the "incomplete" schemas without any nil checks here:

https://github.com/hashicorp/terraform/blob/26ead07b68c106356b8d772fbeb36efd27949862/internal/plugin6/convert/schema.go#L104

Then it seems we will need to always send back an "empty" block when there is an error, to prevent the crash for Terraform CLI versions without the error diagnostic and/or nil checks. It'd also be good to update the framework here to report back these diagnostics in the logs, so it is easier to troubleshoot that it at least tried to do the right thing by returning an error diagnostic.

@bflad
Copy link
Contributor

bflad commented Jun 15, 2022

Terraform CLI v1.2.3 released today will at least now correctly report the framework's error diagnostic, rather than panicking:

        Error: failed to read schema for blox_example.test in registry.terraform.io/hashicorp/blox: failed to retrieve schema from provider "registry.terraform.io/hashicorp/blox": Error converting resource schema: The schema for the resource "blox_example" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:
        
        AttributeName("yikes"): unrecognized nesting mode 0

maybe would be good to describe the rules for when NestingMode is necessary.

NestingMode is required for a valid block definition; Terraform supports an enumeration of them (list and set being the only two we want to support today). I will double check the Go documentation for the field and the top level Block type to ensure that it very explicitly states that setting the field is a requirement in the current implementation.

maybe a better design would be for 0 to actually mean "Auto" and by default fallback to List

We've intentionally shied away from automatic behaviors in terraform-plugin-framework where possible as they have tended to cause long term issues in the years of terraform-plugin-sdk maintenance and feature development. Given the above, we'd likely want to lean towards requiring provider developers to be explicit if there is an implementation choice to be made.

Another option rather than direct Block{} struct creation and expecting the NestingMode field to always be defined would be to instead require list/set block helper functions similar to nested attributes.

e.g. for nested attributes today they must be defined via:

Attributes: map[string]tfsdk.Attribute{
	"test_attr": {
		Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{
			"nested_attr": {
				Type:     types.StringType,
				Required: true,
			},
		}),
		Required: true,
	},
},

A similar implementation for blocks could then be something like:

// Design sketch; this is not valid provider code today
Blocks: map[string]tfsdk.Block{
	"test_block": tfsdk.ListBlock(tfsdk.NestedBlock{
		Attributes: map[string]tfsdk.Attribute{
			"nested_attr": {
				Type:     types.StringType,
				Required: true,
			},
		},
		// No NestedMode field to set
	}),
},

Where the only valid Block implementations are created via ListBlock() and SetBlock() functions, which under the hood set NestingMode appropriately. We have some plans to rework some of the schema types, etc. in the future, so this is something we could consider as part that effort.

What do you think?

@abergmeier
Copy link
Author

Thanks for taking the time to explain. I can agree that explicit is sometimes better than implicit.

What do you think?

Maybe I am just dumb but ListNestedAttributes always has me confused what the List is and what the Attribute and on which level. Especially given your example - it just confuses the living hell out of me.
Since I personally strive for code that even is understandable when you have a bad day I would at least advice on renaming exported symbols like ListNestedAttributes and/or document the living hell out of them so it is very clear what you really meant.

@bflad bflad added this to the v0.12.0 milestone Aug 8, 2022
iwarapter pushed a commit to iwarapter/terraform-plugin-framework that referenced this issue Sep 4, 2022
Reference: hashicorp#31
Reference: hashicorp#132
Reference: hashicorp#223
Reference: hashicorp#326
Reference: hashicorp#365
Reference: hashicorp#389

The main goals of this change are:

- Prepare the Go module to support multiple packages that implement concept-specific schema declarations, in particular the `datasource`, `provider`, and `resource` packages
- Continue supporting the existing `tfsdk` package schema implementation with minimal provider developer breaking changes, allowing a deprecation period when the concept-specific schemas are introduced
- Migrate unexported or unintentially exported `tfsdk` schema functionality to internal packages

These goals are accomplished by creating new `internal/fwschema` and `internal/fwxschema` packages, which contain interface types that the provider developer facing packages implement. Currently, the `tfsdk` package is the only implementation, until the design of those concept-specific schema declarations is fully determined. Those designs may include changes to schema implementation details, which cannot be accomplished in the existing `tfsdk` implementation without breaking it and provider developers migrating code to the new packages is a great time to make those types of changes.

The internal interface method design here is purposefully similar to the existing `tfsdk` implementations as we cannot name the methods the same as existing fields and to reduce review burden. After the `tfsdk` implementations are removed, we can consider tidying up the interface methods to drop the `Get` and `Is` method name prefixes.

There are some minor followup changes that will happen either between or during concept-specific schema implementation work, namely:

- Swapping calls `tfsdk` package type `TerraformType()` methods for `AttributeType().TerraformType()` to reduce implementation work for new schema implementations
- Updating unit testing that relies on `tfsdk.Schema` to set up sub-tests via `(*testing.T).Run()` to against multiple implementations

These were not included here to reduce the review burden as they are separate details which can be handled later.
@bflad bflad modified the milestones: v0.12.0, v0.13.0 Sep 6, 2022
@bflad bflad modified the milestones: v0.13.0, v1.0.0 Sep 21, 2022
bflad added a commit that referenced this issue Nov 18, 2022
Reference: #132
Reference: #326
Reference: #437
Reference: #491
Reference: #508
Reference: #532

This change introduces a new `datasource/schema` package, which contains schema interfaces and types relevant to data sources, such as omitting plan modifiers and schema versioning. This new schema implementation also provides strongly typed attributes, nested attributes, and blocks with customizable types. Nested attributes and blocks are exposed with a separate nested object for customization and validation.

The implementation leans heavily on the design choice of the framework being responsible for preventing provider developer runtime errors. The tailored fields no longer expose functionality that is not available for data sources. The framework design will also raise compiler-time errors for errant typing of validators. Upstream updates to terraform-plugin-framework-validators to support the new `schema/validator` package are in progress.

These changes require unit testing, terraform-provider-corner testing, and website documentation updates before being ready for primetime. Now is a great time for general feedback before those efforts begin in earnest.
bflad added a commit that referenced this issue Nov 22, 2022
Reference: #132
Reference: #326
Reference: #437
Reference: #491
Reference: #508
Reference: #532

This change introduces a new `datasource/schema` package, which contains schema interfaces and types relevant to data sources, such as omitting plan modifiers and schema versioning. This new schema implementation also provides strongly typed attributes, nested attributes, and blocks with customizable types. Nested attributes and blocks are exposed with a separate nested object for customization and validation.

The implementation leans heavily on the design choice of the framework being responsible for preventing provider developer runtime errors. The tailored fields no longer expose functionality that is not available for data sources. The framework design will also raise compiler-time errors for errant typing of validators. Upstream updates to terraform-plugin-framework-validators to support the new `schema/validator` package are in progress.

These changes require unit testing, terraform-provider-corner testing, and website documentation updates before being ready for primetime. Now is a great time for general feedback before those efforts begin in earnest.
bflad added a commit that referenced this issue Nov 22, 2022
Reference: #132
Reference: #326
Reference: #437
Reference: #491
Reference: #508
Reference: #532

This change introduces a new `datasource/schema` package, which contains schema interfaces and types relevant to data sources, such as omitting plan modifiers and schema versioning. This new schema implementation also provides strongly typed attributes, nested attributes, and blocks with customizable types. Nested attributes and blocks are exposed with a separate nested object for customization and validation.

The implementation leans heavily on the design choice of the framework being responsible for preventing provider developer runtime errors. The tailored fields no longer expose functionality that is not available for data sources. The framework design will also raise compiler-time errors for errant typing of validators.
bflad added a commit that referenced this issue Nov 28, 2022
Reference: #132
Reference: #326
Reference: #437
Reference: #491
Reference: #508
Reference: #532

This change introduces a new `datasource/schema` package, which contains schema interfaces and types relevant to data sources, such as omitting plan modifiers and schema versioning. This new schema implementation also provides strongly typed attributes, nested attributes, and blocks with customizable types. Nested attributes and blocks are exposed with a separate nested object for customization and validation.

The implementation leans heavily on the design choice of the framework being responsible for preventing provider developer runtime errors. The tailored fields no longer expose functionality that is not available for data sources. The framework design will also raise compiler-time errors for errant typing of validators.

No changes are required for data handling in the `Read` method.

Example definition:

```go
package test

import (
	"context"

	"github.com/bflad/terraform-plugin-framework-type-time/timetypes"
	"github.com/hashicorp/terraform-plugin-framework-validators/float64validator"
	"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
	"github.com/hashicorp/terraform-plugin-framework/datasource"
	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
	"github.com/hashicorp/terraform-plugin-framework/types"
)

type ThingDataSource struct{}

func (d ThingDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"string_attribute": schema.StringAttribute{
				Required: true,
				Validators: []validator.String{
					stringvalidator.LengthBetween(3, 256),
				},
			},
			"custom_string_attribute": schema.StringAttribute{
				CustomType: timetypes.RFC3339Type,
				Optional:   true,
			},
			"list_attribute": schema.ListAttribute{
				ElementType: types.StringType,
				Optional:    true,
			},
			"list_nested_attribute": schema.ListNestedAttribute{
				NestedObject: schema.NestedAttributeObject{
					Attributes: map[string]schema.Attribute{
						"bool_attribute": schema.BoolAttribute{
							Optional: true,
						},
					},
					Validators: []validator.Object{ /*...*/ },
				},
				Optional: true,
				Validators: []validator.List{
					listvalidator.SizeAtMost(2),
				},
			},
			"single_nested_attribute": schema.SingleNestedAttribute{
				Attributes: map[string]schema.Attribute{
					"int64_attribute": schema.Int64Attribute{
						Optional: true,
					},
				},
				Optional: true,
			},
		},
		Blocks: map[string]schema.Block{
			"list_block": schema.ListNestedBlock{
				NestedObject: schema.NestedBlockObject{
					Attributes: map[string]schema.Attribute{
						"float64_attribute": schema.Float64Attribute{
							Optional: true,
							Validators: []validator.Float64{
								float64validator.OneOf(1.2, 2.4),
							},
						},
					},
					Validators: []validator.Object{ /*...*/ },
				},
				Validators: []validator.List{
					listvalidator.SizeAtMost(2),
				},
			},
		},
	}
}
```

To migrate a data source schema:

- Add `github.com/hashicorp/terraform-plugin-framework/datasource/schema` to the `import` statement 
- Switch the `datasource.DataSource` implementation `GetSchema` method to `Schema` whose response includes a `schema.Schema` from the new package.

Prior implementation:

```go
func (d ThingDataSource) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
  return tfsdk.Schema{/* ... */}, nil
}
```

Migrated implementation:

```go
func (d ThingDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
  resp.Schema = schema.Schema{/*...*/}
}
```

- Switch `map[string]tfsdk.Attribute` with `map[string]schema.Attribute`
- Switch `map[string]tfsdk.Block` with `map[string]schema.Block`
- Switch individual attribute and block definitions. Unless the code was already taking advantage of custom attribute types (uncommon so far), the `Type` field will be removed and the map entries must declare the typed implementation, e.g. a `tfsdk.Attribute` with `Type: types.StringType` is equivalent to `schema.StringAttribute`. Custom attribute types can be specified via the `CustomType` field in each of the implementations.

Prior primitive type (`types.BoolType`, `types.Float64Type`, `types.Int64Type`, `types.NumberType`, `types.StringType`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Required: true,
  Type: types.StringType,
}
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.StringAttribute{
  Required: true,
}
```

Prior collection type (`types.ListType`, `types.MapType`, `types.SetType`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Required: true,
  Type: types.ListType{
    ElemType: types.StringType,
  },
}
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.ListAttribute{
  ElementType: types.StringType,
  Required: true,
}
```

Prior single nested attributes type (`tfsdk.SingleNestedAttributes()`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{/*...*/}),
  Required: true,
},
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.SingleNestedAttribute{
  Attributes: map[string]schema.Attribute{/*...*/},
  Required: true,
}
```

Prior collection nested attributes type (`tfsdk.ListNestedAttributes()`, `tfsdk.MapNestedAttributes()`, `tfsdk.SetNestedAttributes()`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{/*...*/}),
  Required: true,
},
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.ListNestedAttribute{
  NestedObject: schema.NestedAttributeObject{
    Attributes: map[string]schema.Attribute{/*...*/},
  },
  Required: true,
}
```

Prior collection blocks type (`tfsdk.Block`) attribute implementation:

```go
// The "tfsdk.Block" could be omitted inside a map[string]tfsdk.Block
tfsdk.Block{
  Attributes: map[string]tfsdk.Attribute{/*...*/},
  Blocks: map[string]tfsdk.Block{/*...*/},
  NestingMode: tfsdk.BlockNestingModeList,
},
```

Migrated implementation:

```go
// The schema.XXXBlock must be declared inside map[string]schema.Block
schema.ListNestedBlock{
  NestedObject: schema.NestedBlockObject{
    Attributes: map[string]schema.Attribute{/*...*/},
    Blocks: map[string]schema.Block{/*...*/},
  },
}
```
bflad added a commit that referenced this issue Nov 28, 2022
Reference: #132
Reference: #326
Reference: #437
Reference: #491
Reference: #508
Reference: #532

This change introduces a new `provider/schema` package, which contains schema interfaces and types relevant to providers, such as omitting `Computed`, plan modifiers, and schema versioning. This new schema implementation also provides strongly typed attributes, nested attributes, and blocks with customizable types. Nested attributes and blocks are exposed with a separate nested object for customization and validation.

The implementation leans heavily on the design choice of the framework being responsible for preventing provider developer runtime errors. The tailored fields no longer expose functionality that is not available for providers. The framework design will also raise compiler-time errors for errant typing of validators.

No changes are required for data handling in the `Read` method.

Example definition:

```go
package test

import (
	"context"

	"github.com/bflad/terraform-plugin-framework-type-time/timetypes"
	"github.com/hashicorp/terraform-plugin-framework-validators/float64validator"
	"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
	"github.com/hashicorp/terraform-plugin-framework/provider"
	"github.com/hashicorp/terraform-plugin-framework/provider/schema"
	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
	"github.com/hashicorp/terraform-plugin-framework/types"
)

type ExampleCloudProvider struct{}

func (p ExampleCloudProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"string_attribute": schema.StringAttribute{
				Required: true,
				Validators: []validator.String{
					stringvalidator.LengthBetween(3, 256),
				},
			},
			"custom_string_attribute": schema.StringAttribute{
				CustomType: timetypes.RFC3339Type,
				Optional:   true,
			},
			"list_attribute": schema.ListAttribute{
				ElementType: types.StringType,
				Optional:    true,
			},
			"list_nested_attribute": schema.ListNestedAttribute{
				NestedObject: schema.NestedAttributeObject{
					Attributes: map[string]schema.Attribute{
						"bool_attribute": schema.BoolAttribute{
							Optional: true,
						},
					},
					Validators: []validator.Object{ /*...*/ },
				},
				Optional: true,
				Validators: []validator.List{
					listvalidator.SizeAtMost(2),
				},
			},
			"single_nested_attribute": schema.SingleNestedAttribute{
				Attributes: map[string]schema.Attribute{
					"int64_attribute": schema.Int64Attribute{
						Optional: true,
					},
				},
				Optional: true,
			},
		},
		Blocks: map[string]schema.Block{
			"list_block": schema.ListNestedBlock{
				NestedObject: schema.NestedBlockObject{
					Attributes: map[string]schema.Attribute{
						"float64_attribute": schema.Float64Attribute{
							Optional: true,
							Validators: []validator.Float64{
								float64validator.OneOf(1.2, 2.4),
							},
						},
					},
					Validators: []validator.Object{ /*...*/ },
				},
				Validators: []validator.List{
					listvalidator.SizeAtMost(2),
				},
			},
		},
	}
}
```

To migrate a provider schema:

- Add `github.com/hashicorp/terraform-plugin-framework/provider/schema` to the `import` statement
- Switch the `provider.Provider` implementation `GetSchema` method to `Schema` whose response includes a `schema.Schema` from the new package.

Prior implementation:

```go
func (p ExampleCloudProvider) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
  return tfsdk.Schema{/* ... */}, nil
}
```

Migrated implementation:

```go
func (p ExampleCloudProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
  resp.Schema = schema.Schema{/*...*/}
}
```

If the provider requires no schema, the method can be entirely empty.

- Switch `map[string]tfsdk.Attribute` with `map[string]schema.Attribute`
- Switch `map[string]tfsdk.Block` with `map[string]schema.Block`
- Switch individual attribute and block definitions. Unless the code was already taking advantage of custom attribute types (uncommon so far), the `Type` field will be removed and the map entries must declare the typed implementation, e.g. a `tfsdk.Attribute` with `Type: types.StringType` is equivalent to `schema.StringAttribute`. Custom attribute types can be specified via the `CustomType` field in each of the implementations.

Prior primitive type (`types.BoolType`, `types.Float64Type`, `types.Int64Type`, `types.NumberType`, `types.StringType`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Required: true,
  Type: types.StringType,
}
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.StringAttribute{
  Required: true,
}
```

Prior collection type (`types.ListType`, `types.MapType`, `types.SetType`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Required: true,
  Type: types.ListType{
    ElemType: types.StringType,
  },
}
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.ListAttribute{
  ElementType: types.StringType,
  Required: true,
}
```

Prior single nested attributes type (`tfsdk.SingleNestedAttributes()`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{/*...*/}),
  Required: true,
},
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.SingleNestedAttribute{
  Attributes: map[string]schema.Attribute{/*...*/},
  Required: true,
}
```

Prior collection nested attributes type (`tfsdk.ListNestedAttributes()`, `tfsdk.MapNestedAttributes()`, `tfsdk.SetNestedAttributes()`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{/*...*/}),
  Required: true,
},
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.ListNestedAttribute{
  NestedObject: schema.NestedAttributeObject{
    Attributes: map[string]schema.Attribute{/*...*/},
  },
  Required: true,
}
```

Prior collection blocks type (`tfsdk.Block`) attribute implementation:

```go
// The "tfsdk.Block" could be omitted inside a map[string]tfsdk.Block
tfsdk.Block{
  Attributes: map[string]tfsdk.Attribute{/*...*/},
  Blocks: map[string]tfsdk.Block{/*...*/},
  NestingMode: tfsdk.BlockNestingModeList,
},
```

Migrated implementation:

```go
// The schema.XXXBlock must be declared inside map[string]schema.Block
schema.ListNestedBlock{
  NestedObject: schema.NestedBlockObject{
    Attributes: map[string]schema.Attribute{/*...*/},
    Blocks: map[string]schema.Block{/*...*/},
  },
}
```
bflad added a commit that referenced this issue Nov 28, 2022
Reference: #132
Reference: #326
Reference: #437
Reference: #491
Reference: #508
Reference: #532

This change introduces a new `provider/schema` package, which contains schema interfaces and types relevant to providers, such as omitting `Computed`, plan modifiers, and schema versioning. This new schema implementation also provides strongly typed attributes, nested attributes, and blocks with customizable types. Nested attributes and blocks are exposed with a separate nested object for customization and validation.

The implementation leans heavily on the design choice of the framework being responsible for preventing provider developer runtime errors. The tailored fields no longer expose functionality that is not available for providers. The framework design will also raise compiler-time errors for errant typing of validators.

No changes are required for data handling in the `Read` method.

Example definition:

```go
package test

import (
	"context"

	"github.com/bflad/terraform-plugin-framework-type-time/timetypes"
	"github.com/hashicorp/terraform-plugin-framework-validators/float64validator"
	"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
	"github.com/hashicorp/terraform-plugin-framework/provider"
	"github.com/hashicorp/terraform-plugin-framework/provider/schema"
	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
	"github.com/hashicorp/terraform-plugin-framework/types"
)

type ExampleCloudProvider struct{}

func (p ExampleCloudProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"string_attribute": schema.StringAttribute{
				Required: true,
				Validators: []validator.String{
					stringvalidator.LengthBetween(3, 256),
				},
			},
			"custom_string_attribute": schema.StringAttribute{
				CustomType: timetypes.RFC3339Type,
				Optional:   true,
			},
			"list_attribute": schema.ListAttribute{
				ElementType: types.StringType,
				Optional:    true,
			},
			"list_nested_attribute": schema.ListNestedAttribute{
				NestedObject: schema.NestedAttributeObject{
					Attributes: map[string]schema.Attribute{
						"bool_attribute": schema.BoolAttribute{
							Optional: true,
						},
					},
					Validators: []validator.Object{ /*...*/ },
				},
				Optional: true,
				Validators: []validator.List{
					listvalidator.SizeAtMost(2),
				},
			},
			"single_nested_attribute": schema.SingleNestedAttribute{
				Attributes: map[string]schema.Attribute{
					"int64_attribute": schema.Int64Attribute{
						Optional: true,
					},
				},
				Optional: true,
			},
		},
		Blocks: map[string]schema.Block{
			"list_block": schema.ListNestedBlock{
				NestedObject: schema.NestedBlockObject{
					Attributes: map[string]schema.Attribute{
						"float64_attribute": schema.Float64Attribute{
							Optional: true,
							Validators: []validator.Float64{
								float64validator.OneOf(1.2, 2.4),
							},
						},
					},
					Validators: []validator.Object{ /*...*/ },
				},
				Validators: []validator.List{
					listvalidator.SizeAtMost(2),
				},
			},
		},
	}
}
```

To migrate a provider schema:

- Add `github.com/hashicorp/terraform-plugin-framework/provider/schema` to the `import` statement
- Switch the `provider.Provider` implementation `GetSchema` method to `Schema` whose response includes a `schema.Schema` from the new package.

Prior implementation:

```go
func (p ExampleCloudProvider) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
  return tfsdk.Schema{/* ... */}, nil
}
```

Migrated implementation:

```go
func (p ExampleCloudProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
  resp.Schema = schema.Schema{/*...*/}
}
```

If the provider requires no schema, the method can be entirely empty.

- Switch `map[string]tfsdk.Attribute` with `map[string]schema.Attribute`
- Switch `map[string]tfsdk.Block` with `map[string]schema.Block`
- Switch individual attribute and block definitions. Unless the code was already taking advantage of custom attribute types (uncommon so far), the `Type` field will be removed and the map entries must declare the typed implementation, e.g. a `tfsdk.Attribute` with `Type: types.StringType` is equivalent to `schema.StringAttribute`. Custom attribute types can be specified via the `CustomType` field in each of the implementations.

Prior primitive type (`types.BoolType`, `types.Float64Type`, `types.Int64Type`, `types.NumberType`, `types.StringType`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Required: true,
  Type: types.StringType,
}
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.StringAttribute{
  Required: true,
}
```

Prior collection type (`types.ListType`, `types.MapType`, `types.SetType`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Required: true,
  Type: types.ListType{
    ElemType: types.StringType,
  },
}
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.ListAttribute{
  ElementType: types.StringType,
  Required: true,
}
```

Prior single nested attributes type (`tfsdk.SingleNestedAttributes()`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{/*...*/}),
  Required: true,
},
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.SingleNestedAttribute{
  Attributes: map[string]schema.Attribute{/*...*/},
  Required: true,
}
```

Prior collection nested attributes type (`tfsdk.ListNestedAttributes()`, `tfsdk.MapNestedAttributes()`, `tfsdk.SetNestedAttributes()`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{/*...*/}),
  Required: true,
},
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.ListNestedAttribute{
  NestedObject: schema.NestedAttributeObject{
    Attributes: map[string]schema.Attribute{/*...*/},
  },
  Required: true,
}
```

Prior collection blocks type (`tfsdk.Block`) attribute implementation:

```go
// The "tfsdk.Block" could be omitted inside a map[string]tfsdk.Block
tfsdk.Block{
  Attributes: map[string]tfsdk.Attribute{/*...*/},
  Blocks: map[string]tfsdk.Block{/*...*/},
  NestingMode: tfsdk.BlockNestingModeList,
},
```

Migrated implementation:

```go
// The schema.XXXBlock must be declared inside map[string]schema.Block
schema.ListNestedBlock{
  NestedObject: schema.NestedBlockObject{
    Attributes: map[string]schema.Attribute{/*...*/},
    Blocks: map[string]schema.Block{/*...*/},
  },
}
```
bflad added a commit that referenced this issue Nov 29, 2022
Reference: #132
Reference: #326
Reference: #437
Reference: #491
Reference: #508
Reference: #532

This change introduces a new `resource/schema` package, which contains schema interfaces and types relevant to resources. This new schema implementation also provides strongly typed attributes, nested attributes, and blocks with customizable types. Nested attributes and blocks are exposed with a separate nested object for customization, plan modification, and validation.

The implementation leans heavily on the design choice of the framework being responsible for preventing provider developer runtime errors. The tailored fields no longer expose functionality that is not available for resources. The framework design will also raise compiler-time errors for errant typing of validators.

No changes are required for data handling in any other `resource.Resource` methods.

Example definition:

```go
package test

import (
	"context"

	"github.com/bflad/terraform-plugin-framework-type-time/timetypes"
	"github.com/hashicorp/terraform-plugin-framework-validators/float64validator"
	"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
	"github.com/hashicorp/terraform-plugin-framework/resource"
	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
	"github.com/hashicorp/terraform-plugin-framework/types"
)

type ThingResource struct{}

func (r ThingResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"string_attribute": schema.StringAttribute{
				Required: true,
				Validators: []validator.String{
					stringvalidator.LengthBetween(3, 256),
				},
			},
			"custom_string_attribute": schema.StringAttribute{
				CustomType: timetypes.RFC3339Type,
				Optional:   true,
			},
			"list_attribute": schema.ListAttribute{
				ElementType: types.StringType,
				Optional:    true,
			},
			"list_nested_attribute": schema.ListNestedAttribute{
				NestedObject: schema.NestedAttributeObject{
					Attributes: map[string]schema.Attribute{
						"bool_attribute": schema.BoolAttribute{
							Optional: true,
						},
					},
					Validators: []validator.Object{ /*...*/ },
				},
				Optional: true,
				Validators: []validator.List{
					listvalidator.SizeAtMost(2),
				},
			},
			"single_nested_attribute": schema.SingleNestedAttribute{
				Attributes: map[string]schema.Attribute{
					"int64_attribute": schema.Int64Attribute{
						Optional: true,
					},
				},
				Optional: true,
			},
		},
		Blocks: map[string]schema.Block{
			"list_block": schema.ListNestedBlock{
				NestedObject: schema.NestedBlockObject{
					Attributes: map[string]schema.Attribute{
						"float64_attribute": schema.Float64Attribute{
							Optional: true,
							Validators: []validator.Float64{
								float64validator.OneOf(1.2, 2.4),
							},
						},
					},
					Validators: []validator.Object{ /*...*/ },
				},
				Validators: []validator.List{
					listvalidator.SizeAtMost(2),
				},
			},
		},
	}
}
```

To migrate a resource schema:

- Add `github.com/hashicorp/terraform-plugin-framework/resource/schema` to the `import` statement
- Switch the `resource.Resource` implementation `GetSchema` method to `Schema` whose response includes a `schema.Schema` from the new package.

Prior implementation:

```go
func (r ThingResource) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
  return tfsdk.Schema{/* ... */}, nil
}
```

Migrated implementation:

```go
func (r ThingResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
  resp.Schema = schema.Schema{/*...*/}
}
```

If the resource requires no schema, the method can be entirely empty.

- Switch `map[string]tfsdk.Attribute` with `map[string]schema.Attribute`
- Switch `map[string]tfsdk.Block` with `map[string]schema.Block`
- Switch individual attribute and block definitions. Unless the code was already taking advantage of custom attribute types (uncommon so far), the `Type` field will be removed and the map entries must declare the typed implementation, e.g. a `tfsdk.Attribute` with `Type: types.StringType` is equivalent to `schema.StringAttribute`. Custom attribute types can be specified via the `CustomType` field in each of the implementations.

Prior primitive type (`types.BoolType`, `types.Float64Type`, `types.Int64Type`, `types.NumberType`, `types.StringType`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Required: true,
  Type: types.StringType,
}
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.StringAttribute{
  Required: true,
}
```

Prior collection type (`types.ListType`, `types.MapType`, `types.SetType`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Required: true,
  Type: types.ListType{
    ElemType: types.StringType,
  },
}
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.ListAttribute{
  ElementType: types.StringType,
  Required: true,
}
```

Prior single nested attributes type (`tfsdk.SingleNestedAttributes()`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{/*...*/}),
  Required: true,
},
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.SingleNestedAttribute{
  Attributes: map[string]schema.Attribute{/*...*/},
  Required: true,
}
```

Prior collection nested attributes type (`tfsdk.ListNestedAttributes()`, `tfsdk.MapNestedAttributes()`, `tfsdk.SetNestedAttributes()`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{/*...*/}),
  Required: true,
},
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.ListNestedAttribute{
  NestedObject: schema.NestedAttributeObject{
    Attributes: map[string]schema.Attribute{/*...*/},
  },
  Required: true,
}
```

Prior collection blocks type (`tfsdk.Block`) attribute implementation:

```go
// The "tfsdk.Block" could be omitted inside a map[string]tfsdk.Block
tfsdk.Block{
  Attributes: map[string]tfsdk.Attribute{/*...*/},
  Blocks: map[string]tfsdk.Block{/*...*/},
  NestingMode: tfsdk.BlockNestingModeList,
},
```

Migrated implementation:

```go
// The schema.XXXBlock must be declared inside map[string]schema.Block
schema.ListNestedBlock{
  NestedObject: schema.NestedBlockObject{
    Attributes: map[string]schema.Attribute{/*...*/},
    Blocks: map[string]schema.Block{/*...*/},
  },
}
```
bflad added a commit that referenced this issue Nov 29, 2022
Reference: #132
Reference: #326
Reference: #437
Reference: #491
Reference: #508
Reference: #532

This change introduces a new `resource/schema` package, which contains schema interfaces and types relevant to resources. This new schema implementation also provides strongly typed attributes, nested attributes, and blocks with customizable types. Nested attributes and blocks are exposed with a separate nested object for customization, plan modification, and validation.

The implementation leans heavily on the design choice of the framework being responsible for preventing provider developer runtime errors. The tailored fields no longer expose functionality that is not available for resources. The framework design will also raise compiler-time errors for errant typing of validators.

No changes are required for data handling in any other `resource.Resource` methods.

Example definition:

```go
package test

import (
	"context"

	"github.com/bflad/terraform-plugin-framework-type-time/timetypes"
	"github.com/hashicorp/terraform-plugin-framework-validators/float64validator"
	"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
	"github.com/hashicorp/terraform-plugin-framework/resource"
	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
	"github.com/hashicorp/terraform-plugin-framework/types"
)

type ThingResource struct{}

func (r ThingResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"string_attribute": schema.StringAttribute{
				Required: true,
				Validators: []validator.String{
					stringvalidator.LengthBetween(3, 256),
				},
			},
			"custom_string_attribute": schema.StringAttribute{
				CustomType: timetypes.RFC3339Type,
				Optional:   true,
			},
			"list_attribute": schema.ListAttribute{
				ElementType: types.StringType,
				Optional:    true,
			},
			"list_nested_attribute": schema.ListNestedAttribute{
				NestedObject: schema.NestedAttributeObject{
					Attributes: map[string]schema.Attribute{
						"bool_attribute": schema.BoolAttribute{
							Optional: true,
						},
					},
					Validators: []validator.Object{ /*...*/ },
				},
				Optional: true,
				Validators: []validator.List{
					listvalidator.SizeAtMost(2),
				},
			},
			"single_nested_attribute": schema.SingleNestedAttribute{
				Attributes: map[string]schema.Attribute{
					"int64_attribute": schema.Int64Attribute{
						Optional: true,
					},
				},
				Optional: true,
			},
		},
		Blocks: map[string]schema.Block{
			"list_block": schema.ListNestedBlock{
				NestedObject: schema.NestedBlockObject{
					Attributes: map[string]schema.Attribute{
						"float64_attribute": schema.Float64Attribute{
							Optional: true,
							Validators: []validator.Float64{
								float64validator.OneOf(1.2, 2.4),
							},
						},
					},
					Validators: []validator.Object{ /*...*/ },
				},
				Validators: []validator.List{
					listvalidator.SizeAtMost(2),
				},
			},
		},
	}
}
```

To migrate a resource schema:

- Add `github.com/hashicorp/terraform-plugin-framework/resource/schema` to the `import` statement
- Switch the `resource.Resource` implementation `GetSchema` method to `Schema` whose response includes a `schema.Schema` from the new package.

Prior implementation:

```go
func (r ThingResource) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
  return tfsdk.Schema{/* ... */}, nil
}
```

Migrated implementation:

```go
func (r ThingResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
  resp.Schema = schema.Schema{/*...*/}
}
```

If the resource requires no schema, the method can be entirely empty.

- Switch `map[string]tfsdk.Attribute` with `map[string]schema.Attribute`
- Switch `map[string]tfsdk.Block` with `map[string]schema.Block`
- Switch individual attribute and block definitions. Unless the code was already taking advantage of custom attribute types (uncommon so far), the `Type` field will be removed and the map entries must declare the typed implementation, e.g. a `tfsdk.Attribute` with `Type: types.StringType` is equivalent to `schema.StringAttribute`. Custom attribute types can be specified via the `CustomType` field in each of the implementations.

Prior primitive type (`types.BoolType`, `types.Float64Type`, `types.Int64Type`, `types.NumberType`, `types.StringType`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Required: true,
  Type: types.StringType,
}
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.StringAttribute{
  Required: true,
}
```

Prior collection type (`types.ListType`, `types.MapType`, `types.SetType`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Required: true,
  Type: types.ListType{
    ElemType: types.StringType,
  },
}
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.ListAttribute{
  ElementType: types.StringType,
  Required: true,
}
```

Prior single nested attributes type (`tfsdk.SingleNestedAttributes()`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{/*...*/}),
  Required: true,
},
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.SingleNestedAttribute{
  Attributes: map[string]schema.Attribute{/*...*/},
  Required: true,
}
```

Prior collection nested attributes type (`tfsdk.ListNestedAttributes()`, `tfsdk.MapNestedAttributes()`, `tfsdk.SetNestedAttributes()`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{/*...*/}),
  Required: true,
},
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.ListNestedAttribute{
  NestedObject: schema.NestedAttributeObject{
    Attributes: map[string]schema.Attribute{/*...*/},
  },
  Required: true,
}
```

Prior collection blocks type (`tfsdk.Block`) attribute implementation:

```go
// The "tfsdk.Block" could be omitted inside a map[string]tfsdk.Block
tfsdk.Block{
  Attributes: map[string]tfsdk.Attribute{/*...*/},
  Blocks: map[string]tfsdk.Block{/*...*/},
  NestingMode: tfsdk.BlockNestingModeList,
},
```

Migrated implementation:

```go
// The schema.XXXBlock must be declared inside map[string]schema.Block
schema.ListNestedBlock{
  NestedObject: schema.NestedBlockObject{
    Attributes: map[string]schema.Attribute{/*...*/},
    Blocks: map[string]schema.Block{/*...*/},
  },
}
```
bflad added a commit that referenced this issue Nov 29, 2022
Reference: #132
Reference: #326
Reference: #437
Reference: #491
Reference: #508
Reference: #532

This change introduces a new `provider/metaschema` package, which contains schema interfaces and types relevant to provider meta schemas, such as omitting `Computed`, `DeprecatedMessage`, `Sensitive`, plan modifiers, validators, blocks, schema descriptions, and schema versioning. This new schema implementation also provides strongly typed attributes and nested attributes with customizable types. Nested attributes are exposed with a separate nested object for customization.

The implementation leans heavily on the design choice of the framework being responsible for preventing provider developer runtime errors. The tailored fields no longer expose functionality that is not available for provider meta schemas.

No changes are required for data handling during requests.

Example definition:

```go
package test

import (
	"context"

	"github.com/bflad/terraform-plugin-framework-type-time/timetypes"
	"github.com/hashicorp/terraform-plugin-framework-validators/float64validator"
	"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
	"github.com/hashicorp/terraform-plugin-framework/provider"
	"github.com/hashicorp/terraform-plugin-framework/provider/metaschema"
	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
	"github.com/hashicorp/terraform-plugin-framework/types"
)

type ExampleCloudProvider struct{}

func (p ExampleCloudProvider) MetaSchema(ctx context.Context, req provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) {
	resp.Schema = metaschema.Schema{
		Attributes: map[string]schema.Attribute{
			"string_attribute": schema.StringAttribute{
				Required: true,
			},
			"custom_string_attribute": schema.StringAttribute{
				CustomType: timetypes.RFC3339Type,
				Optional:   true,
			},
			"list_attribute": schema.ListAttribute{
				ElementType: types.StringType,
				Optional:    true,
			},
			"list_nested_attribute": schema.ListNestedAttribute{
				NestedObject: schema.NestedAttributeObject{
					Attributes: map[string]schema.Attribute{
						"bool_attribute": schema.BoolAttribute{
							Optional: true,
						},
					},
				},
				Optional: true,
			},
			"single_nested_attribute": schema.SingleNestedAttribute{
				Attributes: map[string]schema.Attribute{
					"int64_attribute": schema.Int64Attribute{
						Optional: true,
					},
				},
				Optional: true,
			},
		},
	}
}
```

To migrate a provider meta schema:

- Add `github.com/hashicorp/terraform-plugin-framework/provider/metaschema` to the `import` statement
- Switch the `provider.ProviderWithMetaSchema` implementation `GetMetaSchema` method to `MetaSchema` whose response includes a `metaschema.Schema` from the new package.

Prior implementation:

```go
func (p ExampleCloudProvider) GetMetaSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
  return tfsdk.Schema{/* ... */}, nil
}
```

Migrated implementation:

```go
func (p ExampleCloudProvider) MetaSchema(ctx context.Context, req provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) {
  resp.Schema =  metaschema.Schema{/*...*/}
}
```

If the provider requires no meta schema, the method can be removed entirely.

- Switch `map[string]tfsdk.Attribute` with `map[string]metaschema.Attribute`
- Switch individual attribute  definitions. Unless the code was already taking advantage of custom attribute types (uncommon so far), the `Type` field will be removed and the map entries must declare the typed implementation, e.g. a `tfsdk.Attribute` with `Type: types.StringType` is equivalent to `metaschema.StringAttribute`. Custom attribute types can be specified via the `CustomType` field in each of the implementations.

Prior primitive type (`types.BoolType`, `types.Float64Type`, `types.Int64Type`, `types.NumberType`, `types.StringType`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Required: true,
  Type: types.StringType,
}
```

Migrated implementation:

```go
// The metaschema.XXXAttribute must be declared inside map[string]metaschema.Attribute
schema.StringAttribute{
  Required: true,
}
```

Prior collection type (`types.ListType`, `types.MapType`, `types.SetType`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Required: true,
  Type: types.ListType{
    ElemType: types.StringType,
  },
}
```

Migrated implementation:

```go
// The metaschema.XXXAttribute must be declared inside map[string]metaschema.Attribute
metaschema.ListAttribute{
  ElementType: types.StringType,
  Required: true,
}
```

Prior single nested attributes type (`tfsdk.SingleNestedAttributes()`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{/*...*/}),
  Required: true,
},
```

Migrated implementation:

```go
// The metaschema.XXXAttribute must be declared inside map[string]metaschema.Attribute
metaschema.SingleNestedAttribute{
  Attributes: map[string]metaschema.Attribute{/*...*/},
  Required: true,
}
```

Prior collection nested attributes type (`tfsdk.ListNestedAttributes()`, `tfsdk.MapNestedAttributes()`, `tfsdk.SetNestedAttributes()`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{/*...*/}),
  Required: true,
},
```

Migrated implementation:

```go
// The metaschema.XXXAttribute must be declared inside map[string]metaschema.Attribute
metaschema.ListNestedAttribute{
  NestedObject: metaschema.NestedAttributeObject{
    Attributes: map[string]metaschema.Attribute{/*...*/},
  },
  Required: true,
}
```
bflad added a commit that referenced this issue Nov 29, 2022
Reference: #132
Reference: #326
Reference: #437
Reference: #491
Reference: #546
Reference: #553
Reference: #558
Reference: #562
bflad added a commit that referenced this issue Nov 29, 2022
Reference: #132
Reference: #326
Reference: #437
Reference: #491
Reference: #508
Reference: #532

This change introduces a new `provider/metaschema` package, which contains schema interfaces and types relevant to provider meta schemas, such as omitting `Computed`, `DeprecatedMessage`, `Sensitive`, plan modifiers, validators, blocks, schema descriptions, and schema versioning. This new schema implementation also provides strongly typed attributes and nested attributes with customizable types. Nested attributes are exposed with a separate nested object for customization.

The implementation leans heavily on the design choice of the framework being responsible for preventing provider developer runtime errors. The tailored fields no longer expose functionality that is not available for provider meta schemas.

No changes are required for data handling during requests.

Example definition:

```go
package test

import (
	"context"

	"github.com/bflad/terraform-plugin-framework-type-time/timetypes"
	"github.com/hashicorp/terraform-plugin-framework-validators/float64validator"
	"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
	"github.com/hashicorp/terraform-plugin-framework/provider"
	"github.com/hashicorp/terraform-plugin-framework/provider/metaschema"
	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
	"github.com/hashicorp/terraform-plugin-framework/types"
)

type ExampleCloudProvider struct{}

func (p ExampleCloudProvider) MetaSchema(ctx context.Context, req provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) {
	resp.Schema = metaschema.Schema{
		Attributes: map[string]schema.Attribute{
			"string_attribute": schema.StringAttribute{
				Required: true,
			},
			"custom_string_attribute": schema.StringAttribute{
				CustomType: timetypes.RFC3339Type,
				Optional:   true,
			},
			"list_attribute": schema.ListAttribute{
				ElementType: types.StringType,
				Optional:    true,
			},
			"list_nested_attribute": schema.ListNestedAttribute{
				NestedObject: schema.NestedAttributeObject{
					Attributes: map[string]schema.Attribute{
						"bool_attribute": schema.BoolAttribute{
							Optional: true,
						},
					},
				},
				Optional: true,
			},
			"single_nested_attribute": schema.SingleNestedAttribute{
				Attributes: map[string]schema.Attribute{
					"int64_attribute": schema.Int64Attribute{
						Optional: true,
					},
				},
				Optional: true,
			},
		},
	}
}
```

To migrate a provider meta schema:

- Add `github.com/hashicorp/terraform-plugin-framework/provider/metaschema` to the `import` statement
- Switch the `provider.ProviderWithMetaSchema` implementation `GetMetaSchema` method to `MetaSchema` whose response includes a `metaschema.Schema` from the new package.

Prior implementation:

```go
func (p ExampleCloudProvider) GetMetaSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
  return tfsdk.Schema{/* ... */}, nil
}
```

Migrated implementation:

```go
func (p ExampleCloudProvider) MetaSchema(ctx context.Context, req provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) {
  resp.Schema =  metaschema.Schema{/*...*/}
}
```

If the provider requires no meta schema, the method can be removed entirely.

- Switch `map[string]tfsdk.Attribute` with `map[string]metaschema.Attribute`
- Switch individual attribute  definitions. Unless the code was already taking advantage of custom attribute types (uncommon so far), the `Type` field will be removed and the map entries must declare the typed implementation, e.g. a `tfsdk.Attribute` with `Type: types.StringType` is equivalent to `metaschema.StringAttribute`. Custom attribute types can be specified via the `CustomType` field in each of the implementations.

Prior primitive type (`types.BoolType`, `types.Float64Type`, `types.Int64Type`, `types.NumberType`, `types.StringType`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Required: true,
  Type: types.StringType,
}
```

Migrated implementation:

```go
// The metaschema.XXXAttribute must be declared inside map[string]metaschema.Attribute
schema.StringAttribute{
  Required: true,
}
```

Prior collection type (`types.ListType`, `types.MapType`, `types.SetType`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Required: true,
  Type: types.ListType{
    ElemType: types.StringType,
  },
}
```

Migrated implementation:

```go
// The metaschema.XXXAttribute must be declared inside map[string]metaschema.Attribute
metaschema.ListAttribute{
  ElementType: types.StringType,
  Required: true,
}
```

Prior single nested attributes type (`tfsdk.SingleNestedAttributes()`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{/*...*/}),
  Required: true,
},
```

Migrated implementation:

```go
// The metaschema.XXXAttribute must be declared inside map[string]metaschema.Attribute
metaschema.SingleNestedAttribute{
  Attributes: map[string]metaschema.Attribute{/*...*/},
  Required: true,
}
```

Prior collection nested attributes type (`tfsdk.ListNestedAttributes()`, `tfsdk.MapNestedAttributes()`, `tfsdk.SetNestedAttributes()`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{/*...*/}),
  Required: true,
},
```

Migrated implementation:

```go
// The metaschema.XXXAttribute must be declared inside map[string]metaschema.Attribute
metaschema.ListNestedAttribute{
  NestedObject: metaschema.NestedAttributeObject{
    Attributes: map[string]metaschema.Attribute{/*...*/},
  },
  Required: true,
}
```
bflad added a commit that referenced this issue Nov 29, 2022
Reference: #132
Reference: #326
Reference: #437
Reference: #491
Reference: #546
Reference: #553
Reference: #558
Reference: #562
Reference: #563
bflad added a commit that referenced this issue Nov 29, 2022
Reference: #132
Reference: #326
Reference: #437
Reference: #491
Reference: #546
Reference: #553
Reference: #558
Reference: #562
bflad added a commit that referenced this issue Nov 30, 2022
…564)

Reference: #132
Reference: #326
Reference: #437
Reference: #491
Reference: #546
Reference: #553
Reference: #558
Reference: #562
Reference: #563
bflad added a commit that referenced this issue Dec 13, 2022
Reference: #132
Reference: #326
Reference: #491

Refer to the following pull request descriptions for migration information about migrating to the split schema packages:

- `datasource/schema`: #546
- `provider/schema`: #553
- `resource/schema`: #558
- `provider/metaschema`: #562
bflad added a commit that referenced this issue Dec 13, 2022
Reference: #132
Reference: #326
Reference: #491

Refer to the following pull request descriptions for migration information about migrating to the split schema packages:

- `datasource/schema`: #546
- `provider/schema`: #553
- `resource/schema`: #558
- `provider/metaschema`: #562
bflad added a commit that referenced this issue Dec 13, 2022
Reference: #132
Reference: #326
Reference: #491

Refer to the following pull request descriptions for migration information about migrating to the split schema packages:

- `datasource/schema`: #546
- `provider/schema`: #553
- `resource/schema`: #558
- `provider/metaschema`: #562
bflad added a commit that referenced this issue Dec 13, 2022
Reference: #132
Reference: #326
Reference: #491

Refer to the following pull request descriptions for migration information about migrating to the split schema packages:

- `datasource/schema`: #546
- `provider/schema`: #553
- `resource/schema`: #558
- `provider/metaschema`: #562
@github-actions
Copy link

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues.
If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jan 13, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants