Skip to content

Commit

Permalink
Add support for defining schemas. (#28)
Browse files Browse the repository at this point in the history
This is another attempt at allowing providers to define schemas. It's
born out of the ashes of #11, which gradually grew from baking an apple
pie from scratch into creating the universe. Or however that saying
goes.

This is intentionally limited in scope to just setting up the types for
declaring schemas and the types required by that, namely our attribute
interfaces. Unlike #11, it makes no attempt to use these types for
anything or prove they're the right types; the work done with #11 gives
me confidence that they're a worthwhile direction to pursue.

I'm submitting this as a separate PR to make review easier and to
optimize for mergeability, letting us get some shared types established
while still taking an appropriate amount of time to review the
reflection code that is in our future.
  • Loading branch information
paddycarver committed Jun 1, 2021
1 parent 668dd44 commit 7610f58
Show file tree
Hide file tree
Showing 9 changed files with 330 additions and 15 deletions.
3 changes: 3 additions & 0 deletions .changelog/27.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
Added support for defining schemas and attributes.
```
70 changes: 70 additions & 0 deletions attr/type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package attr

import (
"context"

"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"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.
type Type interface {
// TerraformType returns the tftypes.Type that should be used to
// represent this type. This constrains what user input will be
// accepted and what kind of data can be set in state. The framework
// will use this to translate the Type to something Terraform can
// understand.
TerraformType(context.Context) tftypes.Type

// ValueFromTerraform returns a Value given a tftypes.Value. This is
// meant to convert the tftypes.Value into a more convenient Go type
// for the provider to consume the data with.
ValueFromTerraform(context.Context, tftypes.Value) (Value, error)

// Equal must return true if the Type is considered semantically equal
// to the Type passed as an argument.
Equal(Type) bool
}

// 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.
//
// TODO: don't use tfprotov6.Diagnostic, use our type
Validate(context.Context, tftypes.Value) []*tfprotov6.Diagnostic
}

// 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
// plain text, with no special formatting.
type TypeWithPlaintextDescription interface {
Type

// Description returns a practitioner-friendly explanation of the type
// and the constraints of the data it accepts and returns. It will be
// combined with the Description associated with the Attribute.
Description(context.Context) string
}

// TypeWithMarkdownDescription extends the Type interface to include a
// MarkdownDescription method, used to bundle extra information to include in
// attribute descriptions with the Type. It expects the description to be
// formatted for display with Markdown.
type TypeWithMarkdownDescription interface {
Type

// MarkdownDescription returns a practitioner-friendly explanation of
// the type and the constraints of the data it accepts and returns. It
// will be combined with the MarkdownDescription associated with the
// Attribute.
MarkdownDescription(context.Context) string
}
24 changes: 24 additions & 0 deletions attr/value.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package attr

import (
"context"

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

// Value defines an interface for describing data associated with an attribute.
// Values allow provider developers to specify data in a convenient format, and
// have it transparently be converted to formats Terraform understands.
type Value interface {
// ToTerraformValue returns the data contained in the Value as
// a Go type that tftypes.NewValue will accept.
ToTerraformValue(context.Context) (interface{}, error)

// SetTerraformValue updates the data in Value to match the
// passed tftypes.Value.
SetTerraformValue(context.Context, tftypes.Value) error

// Equal must return true if the Value is considered semantically equal
// to the Value passed as an argument.
Equal(Value) bool
}
12 changes: 0 additions & 12 deletions framework.go

This file was deleted.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ module github.com/hashicorp/terraform-plugin-framework

go 1.16

require github.com/hashicorp/terraform-plugin-go v0.2.1
require github.com/hashicorp/terraform-plugin-go v0.3.0
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-plugin v1.3.0/go.mod h1:F9eH4LrE/ZsRdbwhfjs9k9HoDUwAHnYtXdgmf1AVNs0=
github.com/hashicorp/terraform-plugin-go v0.2.1 h1:EW/R8bB2Zbkjmugzsy1d27yS8/0454b3MtYHkzOknqA=
github.com/hashicorp/terraform-plugin-go v0.2.1/go.mod h1:10V6F3taeDWVAoLlkmArKttR3IULlRWFAGtQIQTIDr4=
github.com/hashicorp/terraform-plugin-go v0.3.0 h1:AJqYzP52JFYl9NABRI7smXI1pNjgR5Q/y2WyVJ/BOZA=
github.com/hashicorp/terraform-plugin-go v0.3.0/go.mod h1:dFHsQMaTLpON2gWhVWT96fvtlc/MF1vSy3OdMhWBzdM=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
Expand Down
62 changes: 62 additions & 0 deletions schema/attribute.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package schema

import "github.com/hashicorp/terraform-plugin-framework/attr"

// Attribute defines the constraints and behaviors of a single field in a
// schema. Attributes are the fields that show up in Terraform state files and
// can be used in configuration files.
type Attribute struct {
// Type indicates what kind of attribute this is. You'll most likely
// want to use one of the types in the types package.
//
// If Type is set, Attributes cannot be.
Type attr.Type

// Attributes can have their own, nested attributes. This nested map of
// attributes behaves exactly like the map of attributes on the Schema
// type.
//
// If Attributes is set, Type cannot be.
Attributes NestedAttributes

// Description is used in various tooling, like the language server, to
// give practitioners more information about what this attribute is,
// what it's for, and how it should be used. It should be written as
// plain text, with no special formatting.
Description string

// MarkdownDescription is used in various tooling, like the
// documentation generator, to give practitioners more information
// about what this attribute is, what it's for, and how it should be
// used. It should be formatted using Markdown.
MarkdownDescription string

// Required indicates whether the practitioner must enter a value for
// this attribute or not. Required and Optional cannot both be true,
// and Required and Computed cannot both be true.
Required bool

// Optional indicates whether the practitioner can choose not to enter
// a value for this attribute or not. Optional and Required cannot both
// be true.
Optional bool

// Computed indicates whether the provider may return its own value for
// this attribute or not. Required and Computed cannot both be true. If
// Required and Optional are both false, Computed must be true, and the
// attribute will be considered "read only" for the practitioner, with
// only the provider able to set its value.
Computed bool

// Sensitive indicates whether the value of this attribute should be
// considered sensitive data. Setting it to true will obscure the value
// in CLI output. Sensitive does not impact how values are stored, and
// practitioners are encouraged to store their state as if the entire
// file is sensitive.
Sensitive bool

// DeprecationMessage defines a message to display to practitioners
// using this attribute, warning them that it is deprecated and
// instructing them on what upgrade steps to take.
DeprecationMessage string
}
149 changes: 149 additions & 0 deletions schema/nested_attributes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package schema

type nestingMode uint8

const (
nestingModeSingle nestingMode = 0
nestingModeList nestingMode = 1
nestingModeSet nestingMode = 2
nestingModeMap nestingMode = 3
)

// NestedAttributes surfaces a group of attributes to nest beneath another
// attribute, and how that nesting should behave. Nesting can have the
// following modes:
//
// * SingleNestedAttributes are nested attributes that represent a struct or
// object; there should only be one instance of them nested beneath that
// specific attribute.
//
// * ListNestedAttributes are nested attributes that represent a list of
// structs or objects; there can be multiple instances of them beneath that
// specific attribute.
//
// * SetNestedAttributes are nested attributes that represent a set of structs
// or objects; there can be multiple instances of them beneath that specific
// attribute. Unlike ListNestedAttributes, these nested attributes must have
// unique values.
//
// * MapNestedAttributes are nested attributes that represent a string-indexed
// map of structs or objects; there can be multiple instances of them beneath
// that specific attribute. Unlike ListNestedAttributes, these nested
// attributes must be associated with a unique key. Unlike SetNestedAttributes,
// the key must be explicitly set by the user.
type NestedAttributes interface {
getNestingMode() nestingMode
getAttributes() map[string]Attribute
}

type nestedAttributes map[string]Attribute

func (n nestedAttributes) getAttributes() map[string]Attribute {
return map[string]Attribute(n)
}

// SingleNestedAttributes nests `attributes` under another attribute, only
// allowing one instance of that group of attributes to appear in the
// configuration.
func SingleNestedAttributes(attributes map[string]Attribute) NestedAttributes {
return singleNestedAttributes{
nestedAttributes(attributes),
}
}

type singleNestedAttributes struct {
nestedAttributes
}

func (s singleNestedAttributes) getNestingMode() nestingMode {
return nestingModeSingle
}

// ListNestedAttributes nests `attributes` under another attribute, allowing
// multiple instances of that group of attributes to appear in the
// configuration. Minimum and maximum numbers of times the group can appear in
// the configuration can be set using `opts`.
func ListNestedAttributes(attributes map[string]Attribute, opts ListNestedAttributesOptions) NestedAttributes {
return listNestedAttributes{
nestedAttributes: nestedAttributes(attributes),
min: opts.MinItems,
max: opts.MaxItems,
}
}

type listNestedAttributes struct {
nestedAttributes

min, max int
}

// ListNestedAttributesOptions captures additional, optional parameters for
// ListNestedAttributes.
type ListNestedAttributesOptions struct {
MinItems int
MaxItems int
}

func (l listNestedAttributes) getNestingMode() nestingMode {
return nestingModeList
}

// SetNestedAttributes nests `attributes` under another attribute, allowing
// multiple instances of that group of attributes to appear in the
// configuration, while requiring each group of values be unique. Minimum and
// maximum numbers of times the group can appear in the configuration can be
// set using `opts`.
func SetNestedAttributes(attributes map[string]Attribute, opts SetNestedAttributesOptions) NestedAttributes {
return setNestedAttributes{
nestedAttributes: nestedAttributes(attributes),
min: opts.MinItems,
max: opts.MaxItems,
}
}

type setNestedAttributes struct {
nestedAttributes

min, max int
}

// SetNestedAttributesOptions captures additional, optional parameters for
// SetNestedAttributes.
type SetNestedAttributesOptions struct {
MinItems int
MaxItems int
}

func (s setNestedAttributes) getNestingMode() nestingMode {
return nestingModeSet
}

// MapNestedAttributes nests `attributes` under another attribute, allowing
// multiple instances of that group of attributes to appear in the
// configuration. Each group will need to be associated with a unique string by
// the user. Minimum and maximum numbers of times the group can appear in the
// configuration can be set using `opts`.
func MapNestedAttributes(attributes map[string]Attribute, opts MapNestedAttributesOptions) NestedAttributes {
return mapNestedAttributes{
nestedAttributes: nestedAttributes(attributes),
min: opts.MinItems,
max: opts.MaxItems,
}
}

type mapNestedAttributes struct {
nestedAttributes

min, max int
}

// MapNestedAttributesOptions captures additional, optional parameters for
// MapNestedAttributes.
type MapNestedAttributesOptions struct {
MinItems int
MaxItems int
}

func (m mapNestedAttributes) getNestingMode() nestingMode {
return nestingModeMap
}
19 changes: 19 additions & 0 deletions schema/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package schema

// Schema is used to define the shape of practitioner-provider information,
// like resources, data sources, and providers. Think of it as a type
// definition, but for Terraform.
type Schema struct {
// Attributes are the fields inside the resource, provider, or data
// source that the schema is defining. The map key should be the name
// of the attribute, and the body defines how it behaves. Names must
// only contain lowercase letters, numbers, and underscores.
Attributes map[string]Attribute

// Version indicates the current version of the schema. Schemas are
// versioned to help with automatic upgrade process. This is not
// typically required unless there is a change in the schema, such as
// changing an attribute type, that needs manual upgrade handling.
// Versions should only be incremented by one each release.
Version int64
}

0 comments on commit 7610f58

Please sign in to comment.