This repository has been archived by the owner on Oct 3, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ext/typeexpr: HCL extension for "type expressions"
This uses the expression static analysis features to interpret a combination of static calls and static traversals as the description of a type. This is intended for situations where applications need to accept type information from their end-users, providing a concise syntax for doing so. Since this is implemented using static analysis, the type vocabulary is constrained only to keywords representing primitive types and type construction functions for complex types. No other expression elements are allowed. A separate function is provided for parsing type constraints, which allows the additonal keyword "any" to represent the dynamic pseudo-type. Finally, a helper function is provided to convert a type back into a string representation resembling the original input, as an aid to applications that need to produce error messages relating to user-entered types.
- Loading branch information
1 parent
ab87bc9
commit be66a72
Showing
6 changed files
with
855 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# HCL Type Expressions Extension | ||
|
||
This HCL extension defines a convention for describing HCL types using function | ||
call and variable reference syntax, allowing configuration formats to include | ||
type information provided by users. | ||
|
||
The type syntax is processed statically from a hcl.Expression, so it cannot | ||
use any of the usual language operators. This is similar to type expressions | ||
in statically-typed programming languages. | ||
|
||
```hcl | ||
variable "example" { | ||
type = list(string) | ||
} | ||
``` | ||
|
||
The extension is built using the `hcl.ExprAsKeyword` and `hcl.ExprCall` | ||
functions, and so it relies on the underlying syntax to define how "keyword" | ||
and "call" are interpreted. The above shows how they are interpreted in | ||
the HCL native syntax, while the following shows the same information | ||
expressed in JSON: | ||
|
||
```json | ||
{ | ||
"variable": { | ||
"example": { | ||
"type": "list(string)" | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Notice that since we have additional contextual information that we intend | ||
to allow only calls and keywords the JSON syntax is able to parse the given | ||
string directly as an expression, rather than as a template as would be | ||
the case for normal expression evaluation. | ||
|
||
For more information, see [the godoc reference](http://godoc.org/github.com/hashicorp/hcl2/ext/typeexpr). | ||
|
||
## Type Expression Syntax | ||
|
||
When expressed in the native syntax, the following expressions are permitted | ||
in a type expression: | ||
|
||
* `string` - string | ||
* `bool` - boolean | ||
* `number` - number | ||
* `any` - `cty.DynamicPseudoType` (in function `TypeConstraint` only) | ||
* `list(<type_expr>)` - list of the type given as an argument | ||
* `set(<type_expr>)` - set of the type given as an argument | ||
* `map(<type_expr>)` - map of the type given as an argument | ||
* `tuple([<type_exprs...>])` - tuple with the element types given in the single list argument | ||
* `object({<attr_name>=<type_expr>, ...}` - object with the attributes and corresponding types given in the single map argument | ||
|
||
For example: | ||
|
||
* `list(string)` | ||
* `object({"name":string,"age":number})` | ||
* `map(object({"name":string,"age":number}))` | ||
|
||
Note that the object constructor syntax is not fully-general for all possible | ||
object types because it requires the attribute names to be valid identifiers. | ||
In practice it is expected that any time an object type is being fixed for | ||
type checking it will be one that has identifiers as its attributes; object | ||
types with weird attributes generally show up only from arbitrary object | ||
constructors in configuration files, which are usually treated either as maps | ||
or as the dynamic pseudo-type. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
// Package typeexpr extends HCL with a convention for describing HCL types | ||
// within configuration files. | ||
// | ||
// The type syntax is processed statically from a hcl.Expression, so it cannot | ||
// use any of the usual language operators. This is similar to type expressions | ||
// in statically-typed programming languages. | ||
// | ||
// variable "example" { | ||
// type = list(string) | ||
// } | ||
package typeexpr |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
package typeexpr | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/hashicorp/hcl2/hcl" | ||
"github.com/zclconf/go-cty/cty" | ||
) | ||
|
||
const invalidTypeSummary = "Invalid type specification" | ||
|
||
// getType is the internal implementation of both Type and TypeConstraint, | ||
// using the passed flag to distinguish. When constraint is false, the "any" | ||
// keyword will produce an error. | ||
func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) { | ||
// First we'll try for one of our keywords | ||
kw := hcl.ExprAsKeyword(expr) | ||
switch kw { | ||
case "bool": | ||
return cty.Bool, nil | ||
case "string": | ||
return cty.String, nil | ||
case "number": | ||
return cty.Number, nil | ||
case "any": | ||
if constraint { | ||
return cty.DynamicPseudoType, nil | ||
} | ||
return cty.DynamicPseudoType, hcl.Diagnostics{{ | ||
Severity: hcl.DiagError, | ||
Summary: invalidTypeSummary, | ||
Detail: fmt.Sprintf("The keyword %q cannot be used in this type specification: an exact type is required.", kw), | ||
Subject: expr.Range().Ptr(), | ||
}} | ||
case "list", "map", "set": | ||
return cty.DynamicPseudoType, hcl.Diagnostics{{ | ||
Severity: hcl.DiagError, | ||
Summary: invalidTypeSummary, | ||
Detail: fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", kw), | ||
Subject: expr.Range().Ptr(), | ||
}} | ||
case "object": | ||
return cty.DynamicPseudoType, hcl.Diagnostics{{ | ||
Severity: hcl.DiagError, | ||
Summary: invalidTypeSummary, | ||
Detail: "The object type constructor requires one argument specifying the attribute types and values as a map.", | ||
Subject: expr.Range().Ptr(), | ||
}} | ||
case "tuple": | ||
return cty.DynamicPseudoType, hcl.Diagnostics{{ | ||
Severity: hcl.DiagError, | ||
Summary: invalidTypeSummary, | ||
Detail: "The tuple type constructor requires one argument specifying the element types as a list.", | ||
Subject: expr.Range().Ptr(), | ||
}} | ||
case "": | ||
// okay! we'll fall through and try processing as a call, then. | ||
default: | ||
return cty.DynamicPseudoType, hcl.Diagnostics{{ | ||
Severity: hcl.DiagError, | ||
Summary: invalidTypeSummary, | ||
Detail: fmt.Sprintf("The keyword %q is not a valid type specification.", kw), | ||
Subject: expr.Range().Ptr(), | ||
}} | ||
} | ||
|
||
// If we get down here then our expression isn't just a keyword, so we'll | ||
// try to process it as a call instead. | ||
call, diags := hcl.ExprCall(expr) | ||
if diags.HasErrors() { | ||
return cty.DynamicPseudoType, hcl.Diagnostics{{ | ||
Severity: hcl.DiagError, | ||
Summary: invalidTypeSummary, | ||
Detail: "A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).", | ||
Subject: expr.Range().Ptr(), | ||
}} | ||
} | ||
|
||
switch call.Name { | ||
case "bool", "string", "number", "any": | ||
return cty.DynamicPseudoType, hcl.Diagnostics{{ | ||
Severity: hcl.DiagError, | ||
Summary: invalidTypeSummary, | ||
Detail: fmt.Sprintf("Primitive type keyword %q does not expect arguments.", call.Name), | ||
Subject: &call.ArgsRange, | ||
}} | ||
} | ||
|
||
if len(call.Arguments) != 1 { | ||
contextRange := call.ArgsRange | ||
subjectRange := call.ArgsRange | ||
if len(call.Arguments) > 1 { | ||
// If we have too many arguments (as opposed to too _few_) then | ||
// we'll highlight the extraneous arguments as the diagnostic | ||
// subject. | ||
subjectRange = hcl.RangeBetween(call.Arguments[1].Range(), call.Arguments[len(call.Arguments)-1].Range()) | ||
} | ||
|
||
switch call.Name { | ||
case "list", "set", "map": | ||
return cty.DynamicPseudoType, hcl.Diagnostics{{ | ||
Severity: hcl.DiagError, | ||
Summary: invalidTypeSummary, | ||
Detail: fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", call.Name), | ||
Subject: &subjectRange, | ||
Context: &contextRange, | ||
}} | ||
case "object": | ||
return cty.DynamicPseudoType, hcl.Diagnostics{{ | ||
Severity: hcl.DiagError, | ||
Summary: invalidTypeSummary, | ||
Detail: "The object type constructor requires one argument specifying the attribute types and values as a map.", | ||
Subject: &subjectRange, | ||
Context: &contextRange, | ||
}} | ||
case "tuple": | ||
return cty.DynamicPseudoType, hcl.Diagnostics{{ | ||
Severity: hcl.DiagError, | ||
Summary: invalidTypeSummary, | ||
Detail: "The tuple type constructor requires one argument specifying the element types as a list.", | ||
Subject: &subjectRange, | ||
Context: &contextRange, | ||
}} | ||
} | ||
} | ||
|
||
switch call.Name { | ||
|
||
case "list": | ||
ety, diags := getType(call.Arguments[0], constraint) | ||
return cty.List(ety), diags | ||
case "set": | ||
ety, diags := getType(call.Arguments[0], constraint) | ||
return cty.Set(ety), diags | ||
case "map": | ||
ety, diags := getType(call.Arguments[0], constraint) | ||
return cty.Map(ety), diags | ||
case "object": | ||
attrDefs, diags := hcl.ExprMap(call.Arguments[0]) | ||
if diags.HasErrors() { | ||
return cty.DynamicPseudoType, hcl.Diagnostics{{ | ||
Severity: hcl.DiagError, | ||
Summary: invalidTypeSummary, | ||
Detail: "Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.", | ||
Subject: call.Arguments[0].Range().Ptr(), | ||
Context: expr.Range().Ptr(), | ||
}} | ||
} | ||
|
||
atys := make(map[string]cty.Type) | ||
for _, attrDef := range attrDefs { | ||
attrName := hcl.ExprAsKeyword(attrDef.Key) | ||
if attrName == "" { | ||
diags = append(diags, &hcl.Diagnostic{ | ||
Severity: hcl.DiagError, | ||
Summary: invalidTypeSummary, | ||
Detail: "Object constructor map keys must be attribute names.", | ||
Subject: attrDef.Key.Range().Ptr(), | ||
Context: expr.Range().Ptr(), | ||
}) | ||
continue | ||
} | ||
aty, attrDiags := getType(attrDef.Value, constraint) | ||
diags = append(diags, attrDiags...) | ||
atys[attrName] = aty | ||
} | ||
return cty.Object(atys), diags | ||
case "tuple": | ||
elemDefs, diags := hcl.ExprList(call.Arguments[0]) | ||
if diags.HasErrors() { | ||
return cty.DynamicPseudoType, hcl.Diagnostics{{ | ||
Severity: hcl.DiagError, | ||
Summary: invalidTypeSummary, | ||
Detail: "Tuple type constructor requires a list of element types.", | ||
Subject: call.Arguments[0].Range().Ptr(), | ||
Context: expr.Range().Ptr(), | ||
}} | ||
} | ||
etys := make([]cty.Type, len(elemDefs)) | ||
for i, defExpr := range elemDefs { | ||
ety, elemDiags := getType(defExpr, constraint) | ||
diags = append(diags, elemDiags...) | ||
etys[i] = ety | ||
} | ||
return cty.Tuple(etys), diags | ||
default: | ||
// Can't access call.Arguments in this path because we've not validated | ||
// that it contains exactly one expression here. | ||
return cty.DynamicPseudoType, hcl.Diagnostics{{ | ||
Severity: hcl.DiagError, | ||
Summary: invalidTypeSummary, | ||
Detail: fmt.Sprintf("Keyword %q is not a valid type constructor.", call.Name), | ||
Subject: expr.Range().Ptr(), | ||
}} | ||
} | ||
} |
Oops, something went wrong.