# Structs and Interfaces

This document summarises the tradeoffs in the decision between representing Terraform providers, resources, and data sources as structs, or interfaces.

Alternative solutions are first illustrated with example code for reference.

## Semantics and mutability

The Go type we choose for our concept of resource (and provider and data source) can communicate whether we consider these things _types_, or _values_. If a resource is a _type_ in provider code, the framework should offer a resource interface. If a resource is a value, it makes sense for it to be the instance of a struct type offered by the framework.

Values are mutable at runtime, and "local", meant to be a single instance, not an archetypal concept. They can be generated by helpers at startup, from a set of patterns.

Types are immutable at runtime, and "global". Types are not generated through helpers but composed through type composition to share patterns.

## Solution alternatives

### Alternative 1: Structs

Example framework implementation:

```go
type Provider struct {
  Resources func() map[string]ResourceFactory
  DataSources func() map[string]DataSourceFactory
}

type Resource struct {
  Schema func() Schema
  Create func(context.Context, ResourceCreateRequest, ResourceCreateResponse)
  PlanCreate func(context.Context, ResourcePlanCreateRequest, ResourcePlanCreateResponse)
  Read func(context.Context, ResourceReadRequest, ResourceReadResponse)
  Update func(context.Context, ResourceUpdateRequest, ResourceUpdateResponse)
  Delete func(context.Context, ResourceDeleteRequest, ResourceDeleteResponse)
  Import func(context.Context, ResourceImportRequest, ResourceImportResponse)
}
```

### Alternative 2: Interfaces

Example framework implementation:

```go
type Provider interface {
  GetSchema(ctx context.Context) (*tfprotov5.Schema, []*tfprotov5.Diagnostic)
  Configure(context.Context, tftypes.Value) []*tfprotov5.Diagnostic
}

type ProviderWithProviderMeta interface {
  GetMetaSchema(ctx context.Context) (*tfprotov5.Schema, []*tfprotov5.Diagnostic)
}

type ResourceFactory interface {
  NewResource(Provider) (Resource, []*tfprotov5.Diagnostic)
}

type Resource interface {
  GetSchema(context.Context) (*tfprotov5.Schema, []*tfprotov5.Diagnostic)
  Create(context.Context, ResourceCreateRequest, ResourceCreateResponse)
  Read(context.Context, ResourceReadRequest, ResourceReadResponse)
  Update(context.Context, ResourceUpdateRequest, ResourceUpdateResponse)
  Delete(context.Context, ResourceDeleteRequest, ResourceDeleteResponse)
  Import(context.Context, ResourceImportRequest, ResourceImportResponse)
}

type Importer interface {
  Import(context.Context, string) ([]*tfprotov5.ImportedResource, []*tfprotov5.Diagnostic)
}

type Validater interface {
  Validate(context.Context, tftypes.Value) []*tfprotov5.Diagnostic
}

type CustomPlanner interface {
  ModifyPlan(ctx context.Context, priorState, proposedState, config tftypes.Value) (tftypes.Value, []*tfprotov5.Diagnostic)
}
```

### Alternative 3: Mix of structs and interfaces

Example framework implementation:

```go
type Provider struct {
	Resources     map[string]Resource
	ConfigureFunc func(context.Context, *tfprotov5.ConfigureProviderRequest, *tfprotov5.ConfigureProviderResponse, *ProviderState)
}

type Resource struct {
	Schema                *tfprotov5.Schema
	Implementation        func(p ProviderState) ResourceImplementation
}

// ResourceImplementation defines the methods of this resource.
type ResourceImplementation interface {
	Create(context.Context) []*tfprotov5.Diagnostic
	Read(context.Context) []*tfprotov5.Diagnostic
	Update(context.Context) []*tfprotov5.Diagnostic
	Destroy(context.Context) []*tfprotov5.Diagnostic
}
```

## Tradeoffs

### Runtime state

This section concerns stateful data generated during the lifecycle of the provider, not Terraform state.

For example, consider the `meta` parameter in the CRUD functions of [`helper/schema#Resource`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema#Resource), which is conventionally used by providers to store a configured API client. Since this data lives in a framework-owned type and not a provider-owned type, its type cannot be known at compile time and it is represented as `interface{}`. Its type must be asserted at the top of each CRUD method where it is used.

#### Structs

If providers, resources, and data sources are represented as structs in framework code, provider code will instantiate values of those structs. 

Certain struct fields could be populated by the framework: for `Resource`, this could include the equivalent of SDKv2's `meta` value and `TerraformVersion`. Provider code cannot add additional struct fields to store strongly-typed data. This lack of flexibility is a **disadvantage**, although it likely also prevents some anti-patterns.

An abandoned idea involved storing `Config` and `State` on the resource struct. This approach seems strictly worse than including config and state values in CRUD function request and response objects (e.g. `ResourceCreateRequest`), since it would be unclear when and how the framework modified those values.

#### Interfaces

In fulfilling an interface by creating instances of their own provider, resource, and data source struct types, provider developers have the **advantage** of more flexibility in their choice of fields within those structs, and whether they are used to store their own state.

Allowing provider developers to define their own resource types, and construct them with a factory pattern, means that the type assertion on any meta parameter need be done only once, in the factory itself. This is an **advantage**.

A **disadvantage** of the interface approach is that provider-defined structs are now responsible for managing state that framework helpers may need to access. An example of this is `TerraformVersion`, which is included in the `ConfigureProvider` RPC and needs to be available for inclusion in User-Agent HTTP headers via framework helpers that will be called from resource CRUD functions. Assuming `TerraformVersion` is to be stored on `Provider`, it can be set either when the struct is created, by passing arguments to the factory:

```go
func NewProvider(data framework.RuntimeState) Provider {
  return provider{
    terraformVersion: data.TerraformVersion,
  }
}
```

or with explicit methods:

```go
type provider struct {
  framework.Provider
  terraformVersion string
}

func (p *provider) SetTerraformVersion(version string) {
  p.terraformVersion = version
}
```

However the provider-controlled struct is given this runtime state, helpers controlled by the framework need access to `terraformVersion`, so a getter is also required:

```go
func (p provider) GetTerraformVersion() string {
  return p.terraformVersion
}
```

This implementation burden on the provider code can be mitigated by providing a framework-controlled struct that providers are required to embed in their interface implementations. This can be enforced at compile time by requiring an unexported method to be implemented.

Framework code:
```go
type RuntimeData struct {
  terraformVersion string
  ConfigureProviderData interface{}
}

func (r RuntimeData) getRuntimeData() RuntimeData {
  return r
}
```

Provider code:
```go
type provider {
  framework.RuntimeData
}
```

`ConfigureProviderData` here represents other runtime data supplied by the `ConfigureProvider` request. Please see the document on Runtime State for further discussion of what runtime data may be injected and the tradeoffs involved.


### Naming and static analysis

#### Structs

When constructing a value for a struct type (e.g., `Resource`) using a composite literal (e.g., `r := Resource{}`) or assignment to struct field (e.g., `r.Create = ...`), any struct field whose type is a function may be given a value which is a function literal, or a function name identifier, where the function is declared elsewhere. That is, the following two forms are possible:

```go
r := Resource{
  Create: func(ctx context.Context, req ResourceCreateRequest, res ResourceCreateResponse) {},
}
```
and
```go
r := Resource{
  Create: myResourceCreate,
}

func myResourceCreate(ctx context.Context, req ResourceCreateRequest, res ResourceCreateResponse) {}
```

> The second form is conventional when constructing the `Resource` struct from the existing `helper/schema` framework.

The variety of ways struct methods can be given a value makes the task of static code analysis harder, as it is harder to identify which fragment of code contains the body of the function belonging to `r.Create`. The convention of declaring named functions introduces many long function names to the global namespace. This is a **disadvantage**.

#### Interfaces

Receiver methods fulfilling the method set of an interface are always named the same: for example, in solution 2, an implementation of the `Resource` interface will define a method called `Create`. This is an **advantage** in the case of static analysis, since method names signify intent.

### Documentation

#### Structs

When navigating documentation, struct value functions are not grouped at all. This is a **disadvantage**.

> For example, see the documentation for [`helper/schema#CreateFunc`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/helper/schema#CreateFunc).

#### Interfaces

When navigating documentation, the functions of an interface are grouped together by their namespace. This is an **advantage**.

> For example, see the documentation for [`net/http#Server`](https://pkg.go.dev/net/http#Server).

### Compile-time checks

#### Structs

There is no check at compile time that a struct has non-nil values for every field.

For example, given the following struct:

```go
type Resource struct {
  Create func() error
}
```

And an instance `r` of that struct type:

```go
r := Resource{}
```

A runtime panic will occur due to nil pointer dereference if the SDK calls `r.Create()`. The SDK must check at runtime that struct functions are set appropriately, so developers do not get feedback at compile time if they have omitted a required function. This is a **disadvantage**.

On the other hand, a compile-time error will occur if a provider developer has made a typo and defined, for example, a `Craete` method instead of `Create`. This is an **advantage**.

#### Interfaces

Compile-time assertions can be made in provider code that a struct instance satisfies a particular interface. For example, the following resource must satisfy `framework.Resource` and the compiler will error if any method name has a typo:

```go
type computeInstanceResource struct {
}

var _ framework.Resource = computeInstanceResource{}
```

While the need for boilerplate type assertion code in the provider could be considered a disadvantage, omitting such assertions is no worse than the struct case, so the ability to make compile-time assertions in the case of interfaces is an **advantage**. 

Should we want to create decorators, i.e. wrapped implementations of interfaces, an interface-based approach is at a **disadvantage**, as the framework must implement all methods even if only a single one needs to be modified.

### Discoverability

#### Structs

It is easy for users to see what methods they can implement for a struct, which is an **advantage**, but since all relevant methods, including alternative deprecated or preferred versions (see Compatibility), will be attached to the same struct, it may be difficult for users to see at a glance which methods they should implement. This is a **disadvantage**.

#### Interfaces

A full implementation of a `Resource` may require reference to several different interfaces: for example, `Resource`, `Validater`, and `Importer`. While this is a disadvantage in terms of discoverability, a `RecommendedResource` interface capturing the majority of use cases can be provided, and users advised to implement this interface. The extra documentation required for this approach makes it a little less discoverable than the straightforward struct case, putting it at a **slight disadvantage**.

### Deprecation

#### Structs

Framework developers can add additional methods to an existing struct and document the old ones as deprecated, without forcing provider developers to use a specific combination of old and new methods. 

> This was the approach taken in `helper/schema` when two new sets of CRUD functions were added: `CreateContext` etc ([#276](https://github.com/hashicorp/terraform-plugin-sdk/pull/276)), and `CreateWithoutTimeout` etc ([#723](https://github.com/hashicorp/terraform-plugin-sdk/pull/723)). 
 
As is evident from the `helper/schema` case, such new methods must have unique and therefore rather verbose names within the existing `Resource` struct, putting the struct approach at a **slight disadvantage** relative to interfaces.

At times, framework developers will want to communicate to users that a particular _set_ of struct methods is deprecated, and that users should use a new set or some combination from several new sets, as in the case of the `helper/schema` CRUD functions in the above example. It is a **disadvantage** of the struct approach that communicating this to users can only be done by grouping methods together in the struct documentation.

#### Interfaces

Continuing the above example, it is an **advantage** of the interface-based approach that new sets of methods on the `Resource` type could be grouped into interfaces to communicate their intended use and recommended combinations. We might define the following:

```go
// Resource defines the methods of this resource.
//
// Deprecated: use ResourceWithContext instead.
type Resource interface {
	Create() error
	Read() error
	Update() error
	Destroy() error
}

// ResourceWithContext defines the methods of this resource.
type ResourceWithContext interface {
	Create(context.Context) []*tfprotov5.Diagnostic
	Read(context.Context) []*tfprotov5.Diagnostic
	Update(context.Context) []*tfprotov5.Diagnostic
	Destroy(context.Context) []*tfprotov5.Diagnostic
}
```

## Recommendations

We're recommending that interfaces be used for resources, data sources, and
providers. We feel that interfaces provide clearer boundaries for the SDK's
behavior and offer a better communication method for making explicit its
expectations. We also believe that interfaces allow more provider code to be
strongly-typed and benefit from compiler assistance.

There is some concern that interfaces allow providers too much freedom to store
inappropriate runtime state on their types, given the flexibility of defining
their own types. This is a thing we've run into in `helper/schema`, and a very
real danger, and something we should keep an eye on mitigating as we implement.
A clear and obvious tool, though not by any means a complete solution, is
documenting what is expected and appropriate, and what is unexpected and
unsupported in the GoDoc comments for the places where users have the ability
to create issues for themselves.

We feel that structs are the most obvious entrypoint, and offer the best
first-five-minutes experience to provider developers, because they're filling
in blanks instead of defining their own types. However, we feel that interfaces
offer the best first-hour experience to provider developers, because of the
increased prevalence for needing to assert types when using structs. We also
feel that interfaces allow provider developers to impose more structure on the
codebase they're developing, as they're able to use their own types, which
makes ongoing maintenance of providers easier. We're hoping that scaffolding
and other tooling can help improve the first-five-minutes experience in the
future.

We feel like interfaces will encourage unit testability of code, both the
framework's code and the provider's, as they make it more difficult for the
framework to build up types that provider developers can not construct
themselves.
