Skip to content

Commit

Permalink
path+website: Additional Go documentation and initial website documen…
Browse files Browse the repository at this point in the history
…tation for paths and path expressions (hashicorp#413)

Reference: hashicorp#81
  • Loading branch information
bflad authored Jul 18, 2022
1 parent bd0d5f1 commit bdb9f86
Show file tree
Hide file tree
Showing 8 changed files with 882 additions and 4 deletions.
41 changes: 40 additions & 1 deletion path/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,43 @@ import (
)

// Expression represents an attribute path with expression steps, which can
// represent zero, one, or more actual Paths.
// represent zero, one, or more actual paths in schema data. This logic is
// either based on an absolute path starting at the root of the schema data,
// similar to Path, or a relative path which is intended to be merged with an
// existing absolute path.
//
// Use the MatchRoot() function to create an Expression for an absolute path
// with an initial AtName() step. Use the MatchRelative() function to create
// an Expression for a relative path, which will be merged with an existing
// absolute path.
//
// Similar to Path, Expression functionality has some overlapping method names
// and follows a builder pattern, which allows for chaining method calls to
// construct a full expression. The available traversal steps after Expression
// creation are:
//
// - AtAnyListIndex(): Step into a list at any index
// - AtAnyMapKey(): Step into a map at any key
// - AtAnySetValue(): Step into a set at any attr.Value element
// - AtListIndex(): Step into a list at a specific index
// - AtMapKey(): Step into a map at a specific key
// - AtName(): Step into an attribute or block with a specific name
// - AtParent(): Step backwards one step
// - AtSetValue(): Step into a set at a specific attr.Value element
//
// For example, to express any list element with a root list attribute named
// "some_attribute":
//
// path.MatchRoot("some_attribute").AtAnyListIndex()
//
// An Expression is generally preferable over a Path in schema-defined
// functionality that is intended to accept paths as parameters, such as
// attribute validators and attribute plan modifiers, since it allows consumers
// to support relative paths. Use the Merge() or MergeExpressions() method to
// combine the current attribute path expression with those expression(s).
//
// To find Paths from an Expression in schema based data structures, such as
// tfsdk.Config, tfsdk.Plan, and tfsdk.State, use their PathMatches() method.
type Expression struct {
// root stores whether an expression was intentionally created to start
// from the root of the data. This is used with Merge to overwrite steps
Expand Down Expand Up @@ -163,6 +199,9 @@ func (e Expression) Merge(other Expression) Expression {

// MergeExpressions returns collection of expressions that calls Merge() on
// the current expression with each of the others.
//
// If no Expression are given, then it will return a collection of expressions
// containing only the current expression.
func (e Expression) MergeExpressions(others ...Expression) Expressions {
var result Expressions

Expand Down
2 changes: 2 additions & 0 deletions path/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package path
import "strings"

// Expressions is a collection of attribute path expressions.
//
// Refer to the Expression documentation for more details about intended usage.
type Expressions []Expression

// Append adds the given Expressions to the collection without duplication and
Expand Down
32 changes: 29 additions & 3 deletions path/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,33 @@ import (
"github.com/hashicorp/terraform-plugin-framework/attr"
)

// Path represents an attribute path with exact steps. Only exact path
// transversals are supported with this implementation as it must remain
// compatible with all protocol implementations.
// Path represents exact traversal steps into a schema or schema-based data.
// These steps always start from the root of the schema, which is an object
// with zero or more attributes and blocks.
//
// Use the Root() function to create a Path with an initial AtName() step. Path
// functionality follows a builder pattern, which allows for chaining method
// calls to construct a full path. The available traversal steps after Path
// creation are:
//
// - AtListIndex(): Step into a list at a specific 0-based index
// - AtMapKey(): Step into a map at a specific key
// - AtName(): Step into an attribute or block with a specific name
// - AtSetValue(): Step into a set at a specific attr.Value element
//
// For example, to represent the first list element with a root list attribute
// named "some_attribute":
//
// path.MatchRoot("some_attribute").AtListIndex(0)
//
// Path is used for functionality which must exactly match the underlying
// schema structure and types, such as diagnostics that are intended for a
// specific attribute or working with specific attribute values in a schema
// based data structure such as tfsdk.Config, tfsdk.Plan, or tfsdk.State.
//
// Refer to Expression for situations where relative or wildcard step logic is
// desirable for schema defined functionality, such as attribute validators or
// attribute plan modifiers.
type Path struct {
// steps is the transversals included with the path. In general, operations
// against the path should protect against modification of the original.
Expand All @@ -15,6 +39,8 @@ type Path struct {

// AtListIndex returns a copied path with a new list index step at the end.
// The returned path is safe to modify without affecting the original.
//
// List indices are 0-based. The first element of a list is 0.
func (p Path) AtListIndex(index int) Path {
copiedPath := p.Copy()

Expand Down
2 changes: 2 additions & 0 deletions path/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package path
import "strings"

// Paths is a collection of exact attribute paths.
//
// Refer to the Path documentation for more details about intended usage.
type Paths []Path

// Append adds the given Paths to the collection without duplication and
Expand Down
8 changes: 8 additions & 0 deletions website/data/plugin-framework-nav-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@
"title": "Schemas",
"path": "schemas"
},
{
"title": "Paths",
"path": "paths"
},
{
"title": "Path Expressions",
"path": "path-expressions"
},
{
"title": "Attribute Types",
"path": "types"
Expand Down
182 changes: 182 additions & 0 deletions website/docs/plugin/framework/path-expressions.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
---
page_title: 'Plugin Development - Framework: Path Expressions'
description: >-
How to implement path expressions in the provider development framework.
Path expressions are logic built on top of paths, which may represent one or
more actual paths within schema data.
---

# Path Expressions

Path expressions are logic built on top of [paths](/plugin/framework/paths), which may represent one or more actual paths within a schema or schema-based data. Expressions enable providers to work outside the restrictions of absolute paths and steps.

## Usage

Example uses include:

- [Path based attribute validators](/plugin/framework/validation#path-based-attribute-validators), such as those in the [`terraform-plugin-framework-validators` module `schemavalidator` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/schemavalidator).

Use cases which require exact locations, such as [diagnostics](/plugin/framework/diagnostics), implement [paths](/plugin/framework/paths).

## Concepts

Path expressions are an abstraction above [paths](/plugin/framework/paths). This page assumes knowledge of path concepts and implementations.

At its core, expressions implement the following on top of paths:

- Information that designates whether path information is intended to be absolute, similar to paths, or relative, where it is assumed it will be merged with other absolute path information.
- Parent steps, which enables backwards traversal towards the root of a schema in relative paths, after being merged with other absolute path information.
- Path matching, which enables path information to logically return one or more actual paths.

Similar to paths, expressions are built using steps. There are expression steps which directly correspond to exact path steps, such as `AtListIndex()`, `AtMapKey()`, `AtName()`, `AtSetValue()`. Their implementation is the same. However, there are additional expression steps, such as `AtAnyListIndex()`, which cannot be represented in paths due to the potential for ambiguity.

Path matching is the notion that each expression step implements a method that logically determines if a given exact path step should match. For example, the `AtAnyListIndex()` expression step will accept any exact path step for a list index. Path matching with an expression is a collection of matching each expression step against each exact path step, after resolving any potential parent steps.

Every path expression must align with the schema definition or an error diagnostic will be raised when working with path matching within the framework. Provider-defined functionality that is schema-based, such as attribute validation and attribute plan modification, are provided an accurate current path expression since that functionality would not be able to determine its own path expression.

## Building Path Expressions

The framework implementation for path expressions is in the [`path` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/path), with the [`path.Expression` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/path#Expression) being the main provider developer interaction point.

### Building Absolute Path Expressions

Call the [`path.MatchRoot()` function](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/path#MatchRoot) with an attribute name or block name at the root of the schema to begin an absolute path expression.

Given this example schema with a root attribute named `example_root_attribute`:

```go
tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"example_root_attribute": {
Required: true,
Type: types.StringType,
},
},
}
```

The call to `path.MatchRoot()` which matches the location of `example_root_attribute` string value is:

```go
path.MatchRoot("example_root_attribute")
```

For blocks, the beginning of a path expression is similarly defined. Attribute and block names cannot overlap, so the framework automatically handles whether a path expression is referring to an attribute or block to start.

Given this example schema with a root block named `example_root_block`:

```go
tfsdk.Schema{
Blocks: map[string]tfsdk.Block{
"example_root_block": {
Attributes: map[string]tfsdk.Attribute{/* ... */},
NestingMode: tfsdk.BlockNestingModeList,
},
},
}
```

The call to `path.MatchRoot()` which matches the location of `example_root_block` list value is:

```go
path.MatchRoot("example_root_block")
```

Once a `path.Expression` is started, it supports a builder pattern, which allows for chaining method calls to construct a full path.

This example shows a hypothetical path expression that points to any element of a list attribute to highlight the builder pattern:

```go
path.MatchRoot("example_list_attribute").AtAnyListIndex()
```

This pattern can be extended to as many calls as necessary. The [Building Expression Steps section](#building-expression-steps) covers the different framework schema types and any special path step methods.

### Building Relative Path Expressions

Relative path expressions are, by nature, contextual to the actual path where they are defined in a schema. Call the [`path.MatchRelative()` function](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/path#MatchRelative) to begin a relative path expression.

This example shows a relative path expression which references a child attribute:

```go
tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"root_list_attribute": {
Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{
"nested_list_attribute": {
Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{
"deeply_nested_string_attribute": {
Required: true,
Type: types.StringType,
},
}),
Required: true,
Validators: []tfsdk.AttributeValidator{
exampleValidatorThatAcceptsExpressions(
path.MatchRelative().AtAnyListIndex().AtName("deeply_nested_string_attribute"),
),
},
},
"nested_string_attribute": {
Required: true,
Type: types.StringType,
},
}),
Required: true,
},
},
}
```

This example shows a relative path expression which references a different attribute within the same list index:

```go
tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"root_list_attribute": {
Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{
"nested_list_attribute": {
Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{
"deeply_nested_string_attribute": {
Required: true,
Type: types.StringType,
},
}),
Required: true,
Validators: []tfsdk.AttributeValidator{
exampleValidatorThatAcceptsExpressions(
path.MatchRelative().AtParent().AtName("nested_string_attribute"),
),
},
},
"nested_string_attribute": {
Required: true,
Type: types.StringType,
},
}),
Required: true,
},
},
}
```

### Building Expression Steps

Expressions follow similar schema type rules as paths, in particular [Building Attribute Paths](/plugin/framework/paths#building-attribute-paths), [Building Nested Attribute Paths](/plugin/framework/paths#building-nested-attribute-paths), and [Building Block Paths](/plugin/framework/paths#building-block-paths).

The following list shows the [`path.Expression` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/path#Expression) methods that behave similar to [`path.Path` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/path#Path) methods.

- `AtListIndex()`
- `AtMapKey()`
- `AtName()`
- `AtSetValue()`

The following table shows the additional [`path.Expression` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/path#Expression) methods and their descriptions.

| Expression Method | Description |
| ------------------ | ----------- |
| `AtAnyListIndex()` | Will return matches for any list index. Can be used anywhere `AtListIndex()` can be used. |
| `AtAnyMapKey()` | Will return matches for any map key. Can be used anywhere `AtMapKey()` can be used. |
| `AtAnySetValue()` | Will return matches for any set value. Can be used anywhere `AtSetValue()` can be used. |
| `AtParent()` | Will remove the last expression step, or put differently, will match the path closer to the root of the schema. |

Loading

0 comments on commit bdb9f86

Please sign in to comment.