vendor: go get github.com/hashicorp/hcl/v2@v2.3.0

This brings in the new HCL extension functions "try", "can", and
"convert", along with the underlying HCL and cty infrastructure that allow
them to work.
This commit is contained in:
Martin Atkins 2020-01-03 13:29:32 -08:00
parent 7500fa7a39
commit b62e9a7227
32 changed files with 1539 additions and 64 deletions

4
go.mod
View File

@ -69,7 +69,7 @@ require (
github.com/hashicorp/go-uuid v1.0.1
github.com/hashicorp/go-version v1.2.0
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f
github.com/hashicorp/hcl/v2 v2.2.0
github.com/hashicorp/hcl/v2 v2.3.0
github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590
github.com/hashicorp/memberlist v0.1.0 // indirect
github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb // indirect
@ -120,7 +120,7 @@ require (
github.com/xanzy/ssh-agent v0.2.1
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect
github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557
github.com/zclconf/go-cty v1.1.1
github.com/zclconf/go-cty v1.2.1
github.com/zclconf/go-cty-yaml v1.0.1
go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect

10
go.sum
View File

@ -239,8 +239,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+DbLISwf2B8WXEolNRA8BGCwI9jws=
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90=
github.com/hashicorp/hcl/v2 v2.2.0 h1:ZQ1eNLggMfTyFBhV8swxT081mlaRjr4EG85NEjjLB84=
github.com/hashicorp/hcl/v2 v2.2.0/go.mod h1:MD4q2LOluJ5pRwTVkCXmJOY7ODWDXVXGVB8LY0t7wig=
github.com/hashicorp/hcl/v2 v2.3.0 h1:iRly8YaMwTBAKhn1Ybk7VSdzbnopghktCD031P8ggUE=
github.com/hashicorp/hcl/v2 v2.3.0/go.mod h1:d+FwDBbOLvpAM3Z6J7gPj/VoAGkNe/gm352ZhjJ/Zv8=
github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590 h1:2yzhWGdgQUWZUCNK+AoO35V+HTsgEmcM4J9IkArh7PI=
github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590/go.mod h1:n2TSygSNwsLJ76m8qFXTSc7beTb+auJxYdqrnoqwZWE=
github.com/hashicorp/memberlist v0.1.0 h1:qSsCiC0WYD39lbSitKNt40e30uorm2Ss/d4JGU1hzH8=
@ -412,8 +412,10 @@ github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557/go.mod h1:ce1O1j6Ut
github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty v1.1.0 h1:uJwc9HiBOCpoKIObTQaLR+tsEXx1HBHnOsOOpcdhZgw=
github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty v1.1.1 h1:Shl2p9Dat0cqJfXu0DZa+cOTRPhXQjK8IYWD6GVfiqo=
github.com/zclconf/go-cty v1.1.1/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty v1.2.0 h1:sPHsy7ADcIZQP3vILvTjrh74ZA175TFP5vqiNK1UmlI=
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty v1.2.1 h1:vGMsygfmeCl4Xb6OA5U5XVAaQZ69FvoG7X2jUtQujb8=
github.com/zclconf/go-cty v1.2.1/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty-yaml v1.0.1 h1:up11wlgAaDvlAGENcFDnZgkn0qUJurso7k6EpURKNF8=
github.com/zclconf/go-cty-yaml v1.0.1/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=

View File

@ -1,5 +1,15 @@
# HCL Changelog
## v2.3.0 (Jan 3, 2020)
### Enhancements
* ext/tryfunc: Optional functions `try` and `can` to include in your `hcl.EvalContext` when evaluating expressions, which allow users to make decisions based on the success of expressions. ([#330](https://github.com/hashicorp/hcl/pull/330))
* ext/typeexpr: Now has an optional function `convert` which you can include in your `hcl.EvalContext` when evaluating expressions, allowing users to convert values to specific type constraints using the type constraint expression syntax. ([#330](https://github.com/hashicorp/hcl/pull/330))
* ext/typeexpr: A new `cty` capsule type `typeexpr.TypeConstraintType` which, when used as either a type constraint for a function parameter or as a type constraint for a `hcldec` attribute specification will cause the given expression to be interpreted as a type constraint expression rather than a value expression. ([#330](https://github.com/hashicorp/hcl/pull/330))
* ext/customdecode: An optional extension that allows overriding the static decoding behavior for expressions either in function arguments or `hcldec` attribute specifications. ([#330](https://github.com/hashicorp/hcl/pull/330))
* ext/customdecode: New `cty` capsuletypes `customdecode.ExpressionType` and `customdecode.ExpressionClosureType` which, when used as either a type constraint for a function parameter or as a type constraint for a `hcldec` attribute specification will cause the given expression (and, for the closure type, also the `hcl.EvalContext` it was evaluated in) to be captured for later analysis, rather than immediately evaluated. ([#330](https://github.com/hashicorp/hcl/pull/330))
## v2.2.0 (Dec 11, 2019)
### Enhancements

View File

@ -0,0 +1,209 @@
# HCL Custom Static Decoding Extension
This HCL extension provides a mechanism for defining arguments in an HCL-based
language whose values are derived using custom decoding rules against the
HCL expression syntax, overriding the usual behavior of normal expression
evaluation.
"Arguments", for the purpose of this extension, currently includes the
following two contexts:
* For applications using `hcldec` for dynamic decoding, a `hcldec.AttrSpec`
or `hcldec.BlockAttrsSpec` can be given a special type constraint that
opts in to custom decoding behavior for the attribute(s) that are selected
by that specification.
* When working with the HCL native expression syntax, a function given in
the `hcl.EvalContext` during evaluation can have parameters with special
type constraints that opt in to custom decoding behavior for the argument
expression associated with that parameter in any call.
The above use-cases are rather abstract, so we'll consider a motivating
real-world example: sometimes we (language designers) need to allow users
to specify type constraints directly in the language itself, such as in
[Terraform's Input Variables](https://www.terraform.io/docs/configuration/variables.html).
Terraform's `variable` blocks include an argument called `type` which takes
a type constraint given using HCL expression building-blocks as defined by
[the HCL `typeexpr` extension](../typeexpr/README.md).
A "type constraint expression" of that sort is not an expression intended to
be evaluated in the usual way. Instead, the physical expression is
deconstructed using [the static analysis operations](../../spec.md#static-analysis)
to produce a `cty.Type` as the result, rather than a `cty.Value`.
The purpose of this Custom Static Decoding Extension, then, is to provide a
bridge to allow that sort of custom decoding to be used via mechanisms that
normally deal in `cty.Value`, such as `hcldec` and native syntax function
calls as listed above.
(Note: [`gohcl`](https://pkg.go.dev/github.com/hashicorp/hcl/v2/gohcl) has
its own mechanism to support this use case, exploiting the fact that it is
working directly with "normal" Go types. Decoding into a struct field of
type `hcl.Expression` obtains the expression directly without evaluating it
first. The Custom Static Decoding Extension is not necessary for that `gohcl`
technique. You can also implement custom decoding by working directly with
the lowest-level HCL API, which separates extraction of and evaluation of
expressions into two steps.)
## Custom Decoding Types
This extension relies on a convention implemented in terms of
[_Capsule Types_ in the underlying `cty` type system](https://github.com/zclconf/go-cty/blob/master/docs/types.md#capsule-types). `cty` allows a capsule type to carry arbitrary
extension metadata values as an aid to creating higher-level abstractions like
this extension.
A custom argument decoding mode, then, is implemented by creating a new `cty`
capsule type that implements the `ExtensionData` custom operation to return
a decoding function when requested. For example:
```go
var keywordType cty.Type
keywordType = cty.CapsuleWithOps("keyword", reflect.TypeOf(""), &cty.CapsuleOps{
ExtensionData: func(key interface{}) interface{} {
switch key {
case customdecode.CustomExpressionDecoder:
return customdecode.CustomExpressionDecoderFunc(
func(expr hcl.Expression, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
var diags hcl.Diagnostics
kw := hcl.ExprAsKeyword(expr)
if kw == "" {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid keyword",
Detail: "A keyword is required",
Subject: expr.Range().Ptr(),
})
return cty.UnkownVal(keywordType), diags
}
return cty.CapsuleVal(keywordType, &kw)
},
)
default:
return nil
}
},
})
```
The boilerplate here is a bit fussy, but the important part for our purposes
is the `case customdecode.CustomExpressionDecoder:` clause, which uses
a custom extension key type defined in this package to recognize when a
component implementing this extension is checking to see if a target type
has a custom decode implementation.
In the above case we've defined a type that decodes expressions as static
keywords, so a keyword like `foo` would decode as an encapsulated `"foo"`
string, while any other sort of expression like `"baz"` or `1 + 1` would
return an error.
We could then use `keywordType` as a type constraint either for a function
parameter or a `hcldec` attribute specification, which would require the
argument for that function parameter or the expression for the matching
attributes to be a static keyword, rather than an arbitrary expression.
For example, in a `hcldec.AttrSpec`:
```go
keywordSpec := &hcldec.AttrSpec{
Name: "keyword",
Type: keywordType,
}
```
The above would accept input like the following and would set its result to
a `cty.Value` of `keywordType`, after decoding:
```hcl
keyword = foo
```
## The Expression and Expression Closure `cty` types
Building on the above, this package also includes two capsule types that use
the above mechanism to allow calling applications to capture expressions
directly and thus defer analysis to a later step, after initial decoding.
The `customdecode.ExpressionType` type encapsulates an `hcl.Expression` alone,
for situations like our type constraint expression example above where it's
the static structure of the expression we want to inspect, and thus any
variables and functions defined in the evaluation context are irrelevant.
The `customdecode.ExpressionClosureType` type encapsulates a
`*customdecode.ExpressionClosure` value, which binds the given expression to
the `hcl.EvalContext` it was asked to evaluate against and thus allows the
receiver of that result to later perform normal evaluation of the expression
with all the same variables and functions that would've been available to it
naturally.
Both of these types can be used as type constraints either for `hcldec`
attribute specifications or for function arguments. Here's an example of
`ExpressionClosureType` to implement a function that can evaluate
an expression with some additional variables defined locally, which we'll
call the `with(...)` function:
```go
var WithFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "variables",
Type: cty.DynamicPseudoType,
},
{
Name: "expression",
Type: customdecode.ExpressionClosureType,
},
},
Type: func(args []cty.Value) (cty.Type, error) {
varsVal := args[0]
exprVal := args[1]
if !varsVal.Type().IsObjectType() {
return cty.NilVal, function.NewArgErrorf(0, "must be an object defining local variables")
}
if !varsVal.IsKnown() {
// We can't predict our result type until the variables object
// is known.
return cty.DynamicPseudoType, nil
}
vars := varsVal.AsValueMap()
closure := customdecode.ExpressionClosureFromVal(exprVal)
result, err := evalWithLocals(vars, closure)
if err != nil {
return cty.NilVal, err
}
return result.Type(), nil
},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
varsVal := args[0]
exprVal := args[1]
vars := varsVal.AsValueMap()
closure := customdecode.ExpressionClosureFromVal(exprVal)
return evalWithLocals(vars, closure)
},
})
func evalWithLocals(locals map[string]cty.Value, closure *customdecode.ExpressionClosure) (cty.Value, error) {
childCtx := closure.EvalContext.NewChild()
childCtx.Variables = locals
val, diags := closure.Expression.Value(childCtx)
if diags.HasErrors() {
return cty.NilVal, function.NewArgErrorf(1, "couldn't evaluate expression: %s", diags.Error())
}
return val, nil
}
```
If the above function were placed into an `hcl.EvalContext` as `with`, it
could be used in a native syntax call to that function as follows:
```hcl
foo = with({name = "Cory"}, "${greeting}, ${name}!")
```
The above assumes a variable in the main context called `greeting`, to which
the `with` function adds `name` before evaluating the expression given in
its second argument. This makes that second argument context-sensitive -- it
would behave differently if the user wrote the same thing somewhere else -- so
this capability should be used with care to make sure it doesn't cause confusion
for the end-users of your language.
There are some other examples of this capability to evaluate expressions in
unusual ways in the `tryfunc` directory that is a sibling of this one.

View File

@ -0,0 +1,56 @@
// Package customdecode contains a HCL extension that allows, in certain
// contexts, expression evaluation to be overridden by custom static analysis.
//
// This mechanism is only supported in certain specific contexts where
// expressions are decoded with a specific target type in mind. For more
// information, see the documentation on CustomExpressionDecoder.
package customdecode
import (
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
)
type customDecoderImpl int
// CustomExpressionDecoder is a value intended to be used as a cty capsule
// type ExtensionData key for capsule types whose values are to be obtained
// by static analysis of an expression rather than normal evaluation of that
// expression.
//
// When a cooperating capsule type is asked for ExtensionData with this key,
// it must return a non-nil CustomExpressionDecoderFunc value.
//
// This mechanism is not universally supported; instead, it's handled in a few
// specific places where expressions are evaluated with the intent of producing
// a cty.Value of a type given by the calling application.
//
// Specifically, this currently works for type constraints given in
// hcldec.AttrSpec and hcldec.BlockAttrsSpec, and it works for arguments to
// function calls in the HCL native syntax. HCL extensions implemented outside
// of the main HCL module may also implement this; consult their own
// documentation for details.
const CustomExpressionDecoder = customDecoderImpl(1)
// CustomExpressionDecoderFunc is the type of value that must be returned by
// a capsule type handling the key CustomExpressionDecoder in its ExtensionData
// implementation.
//
// If no error diagnostics are returned, the result value MUST be of the
// capsule type that the decoder function was derived from. If the returned
// error diagnostics prevent producing a value at all, return cty.NilVal.
type CustomExpressionDecoderFunc func(expr hcl.Expression, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics)
// CustomExpressionDecoderForType takes any cty type and returns its
// custom expression decoder implementation if it has one. If it is not a
// capsule type or it does not implement a custom expression decoder, this
// function returns nil.
func CustomExpressionDecoderForType(ty cty.Type) CustomExpressionDecoderFunc {
if !ty.IsCapsuleType() {
return nil
}
if fn, ok := ty.CapsuleExtensionData(CustomExpressionDecoder).(CustomExpressionDecoderFunc); ok {
return fn
}
return nil
}

View File

@ -0,0 +1,146 @@
package customdecode
import (
"fmt"
"reflect"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
)
// ExpressionType is a cty capsule type that carries hcl.Expression values.
//
// This type implements custom decoding in the most general way possible: it
// just captures whatever expression is given to it, with no further processing
// whatsoever. It could therefore be useful in situations where an application
// must defer processing of the expression content until a later step.
//
// ExpressionType only captures the expression, not the evaluation context it
// was destined to be evaluated in. That means this type can be fine for
// situations where the recipient of the value only intends to do static
// analysis, but ExpressionClosureType is more appropriate in situations where
// the recipient will eventually evaluate the given expression.
var ExpressionType cty.Type
// ExpressionVal returns a new cty value of type ExpressionType, wrapping the
// given expression.
func ExpressionVal(expr hcl.Expression) cty.Value {
return cty.CapsuleVal(ExpressionType, &expr)
}
// ExpressionFromVal returns the expression encapsulated in the given value, or
// panics if the value is not a known value of ExpressionType.
func ExpressionFromVal(v cty.Value) hcl.Expression {
if !v.Type().Equals(ExpressionType) {
panic("value is not of ExpressionType")
}
ptr := v.EncapsulatedValue().(*hcl.Expression)
return *ptr
}
// ExpressionClosureType is a cty capsule type that carries hcl.Expression
// values along with their original evaluation contexts.
//
// This is similar to ExpressionType except that during custom decoding it
// also captures the hcl.EvalContext that was provided, allowing callers to
// evaluate the expression later in the same context where it would originally
// have been evaluated, or a context derived from that one.
var ExpressionClosureType cty.Type
// ExpressionClosure is the type encapsulated in ExpressionClosureType
type ExpressionClosure struct {
Expression hcl.Expression
EvalContext *hcl.EvalContext
}
// ExpressionClosureVal returns a new cty value of type ExpressionClosureType,
// wrapping the given expression closure.
func ExpressionClosureVal(closure *ExpressionClosure) cty.Value {
return cty.CapsuleVal(ExpressionClosureType, closure)
}
// Value evaluates the closure's expression using the closure's EvalContext,
// returning the result.
func (c *ExpressionClosure) Value() (cty.Value, hcl.Diagnostics) {
return c.Expression.Value(c.EvalContext)
}
// ExpressionClosureFromVal returns the expression closure encapsulated in the
// given value, or panics if the value is not a known value of
// ExpressionClosureType.
//
// The caller MUST NOT modify the returned closure or the EvalContext inside
// it. To derive a new EvalContext, either create a child context or make
// a copy.
func ExpressionClosureFromVal(v cty.Value) *ExpressionClosure {
if !v.Type().Equals(ExpressionClosureType) {
panic("value is not of ExpressionClosureType")
}
return v.EncapsulatedValue().(*ExpressionClosure)
}
func init() {
// Getting hold of a reflect.Type for hcl.Expression is a bit tricky because
// it's an interface type, but we can do it with some indirection.
goExpressionType := reflect.TypeOf((*hcl.Expression)(nil)).Elem()
ExpressionType = cty.CapsuleWithOps("expression", goExpressionType, &cty.CapsuleOps{
ExtensionData: func(key interface{}) interface{} {
switch key {
case CustomExpressionDecoder:
return CustomExpressionDecoderFunc(
func(expr hcl.Expression, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
return ExpressionVal(expr), nil
},
)
default:
return nil
}
},
TypeGoString: func(_ reflect.Type) string {
return "customdecode.ExpressionType"
},
GoString: func(raw interface{}) string {
exprPtr := raw.(*hcl.Expression)
return fmt.Sprintf("customdecode.ExpressionVal(%#v)", *exprPtr)
},
RawEquals: func(a, b interface{}) bool {
aPtr := a.(*hcl.Expression)
bPtr := b.(*hcl.Expression)
return reflect.DeepEqual(*aPtr, *bPtr)
},
})
ExpressionClosureType = cty.CapsuleWithOps("expression closure", reflect.TypeOf(ExpressionClosure{}), &cty.CapsuleOps{
ExtensionData: func(key interface{}) interface{} {
switch key {
case CustomExpressionDecoder:
return CustomExpressionDecoderFunc(
func(expr hcl.Expression, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
return ExpressionClosureVal(&ExpressionClosure{
Expression: expr,
EvalContext: ctx,
}), nil
},
)
default:
return nil
}
},
TypeGoString: func(_ reflect.Type) string {
return "customdecode.ExpressionClosureType"
},
GoString: func(raw interface{}) string {
closure := raw.(*ExpressionClosure)
return fmt.Sprintf("customdecode.ExpressionClosureVal(%#v)", closure)
},
RawEquals: func(a, b interface{}) bool {
closureA := a.(*ExpressionClosure)
closureB := b.(*ExpressionClosure)
// The expression itself compares by deep equality, but EvalContexts
// conventionally compare by pointer identity, so we'll comply
// with both conventions here by testing them separately.
return closureA.EvalContext == closureB.EvalContext &&
reflect.DeepEqual(closureA.Expression, closureB.Expression)
},
})
}

View File

@ -65,3 +65,71 @@ 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.
## Type Constraints as Values
Along with defining a convention for writing down types using HCL expression
constructs, this package also includes a mechanism for representing types as
values that can be used as data within an HCL-based language.
`typeexpr.TypeConstraintType` is a
[`cty` capsule type](https://github.com/zclconf/go-cty/blob/master/docs/types.md#capsule-types)
that encapsulates `cty.Type` values. You can construct such a value directly
using the `TypeConstraintVal` function:
```go
tyVal := typeexpr.TypeConstraintVal(cty.String)
// We can unpack the type from a value using TypeConstraintFromVal
ty := typeExpr.TypeConstraintFromVal(tyVal)
```
However, the primary purpose of `typeexpr.TypeConstraintType` is to be
specified as the type constraint for an argument, in which case it serves
as a signal for HCL to treat the argument expression as a type constraint
expression as defined above, rather than as a normal value expression.
"An argument" in the above in practice means the following two locations:
* As the type constraint for a parameter of a cty function that will be
used in an `hcl.EvalContext`. In that case, function calls in the HCL
native expression syntax will require the argument to be valid type constraint
expression syntax and the function implementation will receive a
`TypeConstraintType` value as the argument value for that parameter.
* As the type constraint for a `hcldec.AttrSpec` or `hcldec.BlockAttrsSpec`
when decoding an HCL body using `hcldec`. In that case, the attributes
with that type constraint will be required to be valid type constraint
expression syntax and the result will be a `TypeConstraintType` value.
Note that the special handling of these arguments means that an argument
marked in this way must use the type constraint syntax directly. It is not
valid to pass in a value of `TypeConstraintType` that has been obtained
dynamically via some other expression result.
`TypeConstraintType` is provided with the intent of using it internally within
application code when incorporating type constraint expression syntax into
an HCL-based language, not to be used for dynamic "programming with types". A
calling application could support programming with types by defining its _own_
capsule type, but that is not the purpose of `TypeConstraintType`.
## The "convert" `cty` Function
Building on the `TypeConstraintType` described in the previous section, this
package also provides `typeexpr.ConvertFunc` which is a cty function that
can be placed into a `cty.EvalContext` (conventionally named "convert") in
order to provide a general type conversion function in an HCL-based language:
```hcl
foo = convert("true", bool)
```
The second parameter uses the mechanism described in the previous section to
require its argument to be a type constraint expression rather than a value
expression. In doing so, it allows converting with any type constraint that
can be expressed in this package's type constraint syntax. In the above example,
the `foo` argument would receive a boolean true, or `cty.True` in `cty` terms.
The target type constraint must always be provided statically using inline
type constraint syntax. There is no way to _dynamically_ select a type
constraint using this function.

View File

@ -0,0 +1,118 @@
package typeexpr
import (
"fmt"
"reflect"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/ext/customdecode"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/function"
)
// TypeConstraintType is a cty capsule type that allows cty type constraints to
// be used as values.
//
// If TypeConstraintType is used in a context supporting the
// customdecode.CustomExpressionDecoder extension then it will implement
// expression decoding using the TypeConstraint function, thus allowing
// type expressions to be used in contexts where value expressions might
// normally be expected, such as in arguments to function calls.
var TypeConstraintType cty.Type
// TypeConstraintVal constructs a cty.Value whose type is
// TypeConstraintType.
func TypeConstraintVal(ty cty.Type) cty.Value {
return cty.CapsuleVal(TypeConstraintType, &ty)
}
// TypeConstraintFromVal extracts the type from a cty.Value of
// TypeConstraintType that was previously constructed using TypeConstraintVal.
//
// If the given value isn't a known, non-null value of TypeConstraintType
// then this function will panic.
func TypeConstraintFromVal(v cty.Value) cty.Type {
if !v.Type().Equals(TypeConstraintType) {
panic("value is not of TypeConstraintType")
}
ptr := v.EncapsulatedValue().(*cty.Type)
return *ptr
}
// ConvertFunc is a cty function that implements type conversions.
//
// Its signature is as follows:
// convert(value, type_constraint)
//
// ...where type_constraint is a type constraint expression as defined by
// typeexpr.TypeConstraint.
//
// It relies on HCL's customdecode extension and so it's not suitable for use
// in non-HCL contexts or if you are using a HCL syntax implementation that
// does not support customdecode for function arguments. However, it _is_
// supported for function calls in the HCL native expression syntax.
var ConvertFunc function.Function
func init() {
TypeConstraintType = cty.CapsuleWithOps("type constraint", reflect.TypeOf(cty.Type{}), &cty.CapsuleOps{
ExtensionData: func(key interface{}) interface{} {
switch key {
case customdecode.CustomExpressionDecoder:
return customdecode.CustomExpressionDecoderFunc(
func(expr hcl.Expression, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
ty, diags := TypeConstraint(expr)
if diags.HasErrors() {
return cty.NilVal, diags
}
return TypeConstraintVal(ty), nil
},
)
default:
return nil
}
},
TypeGoString: func(_ reflect.Type) string {
return "typeexpr.TypeConstraintType"
},
GoString: func(raw interface{}) string {
tyPtr := raw.(*cty.Type)
return fmt.Sprintf("typeexpr.TypeConstraintVal(%#v)", *tyPtr)
},
RawEquals: func(a, b interface{}) bool {
aPtr := a.(*cty.Type)
bPtr := b.(*cty.Type)
return (*aPtr).Equals(*bPtr)
},
})
ConvertFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "value",
Type: cty.DynamicPseudoType,
AllowNull: true,
AllowDynamicType: true,
},
{
Name: "type",
Type: TypeConstraintType,
},
},
Type: func(args []cty.Value) (cty.Type, error) {
wantTypePtr := args[1].EncapsulatedValue().(*cty.Type)
got, err := convert.Convert(args[0], *wantTypePtr)
if err != nil {
return cty.NilType, function.NewArgError(0, err)
}
return got.Type(), nil
},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
v, err := convert.Convert(args[0], retType)
if err != nil {
return cty.NilVal, function.NewArgError(0, err)
}
return v, nil
},
})
}

View File

@ -6,7 +6,7 @@ require (
github.com/apparentlymart/go-textseg v1.0.0
github.com/davecgh/go-spew v1.1.1
github.com/go-test/deep v1.0.3
github.com/google/go-cmp v0.2.0
github.com/google/go-cmp v0.3.1
github.com/kr/pretty v0.1.0
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7
@ -14,7 +14,7 @@ require (
github.com/sergi/go-diff v1.0.0
github.com/spf13/pflag v1.0.2
github.com/stretchr/testify v1.2.2 // indirect
github.com/zclconf/go-cty v1.1.1
github.com/zclconf/go-cty v1.2.0
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82 // indirect
golang.org/x/text v0.3.2 // indirect

View File

@ -9,8 +9,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@ -29,8 +29,8 @@ github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/zclconf/go-cty v1.1.1 h1:Shl2p9Dat0cqJfXu0DZa+cOTRPhXQjK8IYWD6GVfiqo=
github.com/zclconf/go-cty v1.1.1/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty v1.2.0 h1:sPHsy7ADcIZQP3vILvTjrh74ZA175TFP5vqiNK1UmlI=
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -6,6 +6,7 @@ import (
"sort"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/ext/customdecode"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/function"
@ -193,6 +194,14 @@ func (s *AttrSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ct
return cty.NullVal(s.Type), nil
}
if decodeFn := customdecode.CustomExpressionDecoderForType(s.Type); decodeFn != nil {
v, diags := decodeFn(attr.Expr, ctx)
if v == cty.NilVal {
v = cty.UnknownVal(s.Type)
}
return v, diags
}
val, diags := attr.Expr.Value(ctx)
convVal, err := convert.Convert(val, s.Type)
@ -1223,6 +1232,16 @@ func (s *BlockAttrsSpec) decode(content *hcl.BodyContent, blockLabels []blockLab
vals := make(map[string]cty.Value, len(attrs))
for name, attr := range attrs {
if decodeFn := customdecode.CustomExpressionDecoderForType(s.ElementType); decodeFn != nil {
attrVal, attrDiags := decodeFn(attr.Expr, ctx)
diags = append(diags, attrDiags...)
if attrVal == cty.NilVal {
attrVal = cty.UnknownVal(s.ElementType)
}
vals[name] = attrVal
continue
}
attrVal, attrDiags := attr.Expr.Value(ctx)
diags = append(diags, attrDiags...)

View File

@ -5,6 +5,7 @@ import (
"sync"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/ext/customdecode"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/function"
@ -350,26 +351,38 @@ func (e *FunctionCallExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnosti
param = varParam
}
val, argDiags := argExpr.Value(ctx)
if len(argDiags) > 0 {
var val cty.Value
if decodeFn := customdecode.CustomExpressionDecoderForType(param.Type); decodeFn != nil {
var argDiags hcl.Diagnostics
val, argDiags = decodeFn(argExpr, ctx)
diags = append(diags, argDiags...)
}
if val == cty.NilVal {
val = cty.UnknownVal(param.Type)
}
} else {
var argDiags hcl.Diagnostics
val, argDiags = argExpr.Value(ctx)
if len(argDiags) > 0 {
diags = append(diags, argDiags...)
}
// Try to convert our value to the parameter type
val, err := convert.Convert(val, param.Type)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid function argument",
Detail: fmt.Sprintf(
"Invalid value for %q parameter: %s.",
param.Name, err,
),
Subject: argExpr.StartRange().Ptr(),
Context: e.Range().Ptr(),
Expression: argExpr,
EvalContext: ctx,
})
// Try to convert our value to the parameter type
var err error
val, err = convert.Convert(val, param.Type)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid function argument",
Detail: fmt.Sprintf(
"Invalid value for %q parameter: %s.",
param.Name, err,
),
Subject: argExpr.StartRange().Ptr(),
Context: e.Range().Ptr(),
Expression: argExpr,
EvalContext: ctx,
})
}
}
argVals[i] = val

View File

@ -9,6 +9,7 @@ type capsuleType struct {
typeImplSigil
Name string
GoType reflect.Type
Ops *CapsuleOps
}
func (t *capsuleType) Equals(other Type) bool {
@ -24,10 +25,22 @@ func (t *capsuleType) FriendlyName(mode friendlyTypeNameMode) string {
}
func (t *capsuleType) GoString() string {
// To get a useful representation of our native type requires some
// shenanigans.
victimVal := reflect.Zero(t.GoType)
return fmt.Sprintf("cty.Capsule(%q, reflect.TypeOf(%#v))", t.Name, victimVal.Interface())
impl := t.Ops.TypeGoString
if impl == nil {
// To get a useful representation of our native type requires some
// shenanigans.
victimVal := reflect.Zero(t.GoType)
if t.Ops == noCapsuleOps {
return fmt.Sprintf("cty.Capsule(%q, reflect.TypeOf(%#v))", t.Name, victimVal.Interface())
} else {
// Including the operations in the output will make this _very_ long,
// so in practice any capsule type with ops ought to provide a
// TypeGoString function to override this with something more
// reasonable.
return fmt.Sprintf("cty.CapsuleWithOps(%q, reflect.TypeOf(%#v), %#v)", t.Name, victimVal.Interface(), t.Ops)
}
}
return impl(t.GoType)
}
// Capsule creates a new Capsule type.
@ -47,8 +60,11 @@ func (t *capsuleType) GoString() string {
// use the same native type.
//
// Each capsule-typed value contains a pointer to a value of the given native
// type. A capsule-typed value supports no operations except equality, and
// equality is implemented by pointer identity of the encapsulated pointer.
// type. A capsule-typed value by default supports no operations except
// equality, and equality is implemented by pointer identity of the
// encapsulated pointer. A capsule type can optionally have its own
// implementations of certain operations if it is created with CapsuleWithOps
// instead of Capsule.
//
// The given name is used as the new type's "friendly name". This can be any
// string in principle, but will usually be a short, all-lowercase name aimed
@ -65,6 +81,29 @@ func Capsule(name string, nativeType reflect.Type) Type {
&capsuleType{
Name: name,
GoType: nativeType,
Ops: noCapsuleOps,
},
}
}
// CapsuleWithOps is like Capsule except the caller may provide an object
// representing some overloaded operation implementations to associate with
// the given capsule type.
//
// All of the other caveats and restrictions for capsule types still apply, but
// overloaded operations can potentially help a capsule type participate better
// in cty operations.
func CapsuleWithOps(name string, nativeType reflect.Type, ops *CapsuleOps) Type {
// Copy the operations to make sure the caller can't modify them after
// we're constructed.
ourOps := *ops
ourOps.assertValid()
return Type{
&capsuleType{
Name: name,
GoType: nativeType,
Ops: &ourOps,
},
}
}

132
vendor/github.com/zclconf/go-cty/cty/capsule_ops.go generated vendored Normal file
View File

@ -0,0 +1,132 @@
package cty
import (
"reflect"
)
// CapsuleOps represents a set of overloaded operations for a capsule type.
//
// Each field is a reference to a function that can either be nil or can be
// set to an implementation of the corresponding operation. If an operation
// function is nil then it isn't supported for the given capsule type.
type CapsuleOps struct {
// GoString provides the GoString implementation for values of the
// corresponding type. Conventionally this should return a string
// representation of an expression that would produce an equivalent
// value.
GoString func(val interface{}) string
// TypeGoString provides the GoString implementation for the corresponding
// capsule type itself.
TypeGoString func(goTy reflect.Type) string
// Equals provides the implementation of the Equals operation. This is
// called only with known, non-null values of the corresponding type,
// but if the corresponding type is a compound type then it must be
// ready to detect and handle nested unknown or null values, usually
// by recursively calling Value.Equals on those nested values.
//
// The result value must always be of type cty.Bool, or the Equals
// operation will panic.
//
// If RawEquals is set without also setting Equals, the RawEquals
// implementation will be used as a fallback implementation. That fallback
// is appropriate only for leaf types that do not contain any nested
// cty.Value that would need to distinguish Equals vs. RawEquals for their
// own equality.
//
// If RawEquals is nil then Equals must also be nil, selecting the default
// pointer-identity comparison instead.
Equals func(a, b interface{}) Value
// RawEquals provides the implementation of the RawEquals operation.
// This is called only with known, non-null values of the corresponding
// type, but if the corresponding type is a compound type then it must be
// ready to detect and handle nested unknown or null values, usually
// by recursively calling Value.RawEquals on those nested values.
//
// If RawEquals is nil, values of the corresponding type are compared by
// pointer identity of the encapsulated value.
RawEquals func(a, b interface{}) bool
// ConversionFrom can provide conversions from the corresponding type to
// some other type when values of the corresponding type are used with
// the "convert" package. (The main cty package does not use this operation.)
//
// This function itself returns a function, allowing it to switch its
// behavior depending on the given source type. Return nil to indicate
// that no such conversion is available.
ConversionFrom func(src Type) func(interface{}, Path) (Value, error)
// ConversionTo can provide conversions to the corresponding type from
// some other type when values of the corresponding type are used with
// the "convert" package. (The main cty package does not use this operation.)
//
// This function itself returns a function, allowing it to switch its
// behavior depending on the given destination type. Return nil to indicate
// that no such conversion is available.
ConversionTo func(dst Type) func(Value, Path) (interface{}, error)
// ExtensionData is an extension point for applications that wish to
// create their own extension features using capsule types.
//
// The key argument is any value that can be compared with Go's ==
// operator, but should be of a named type in a package belonging to the
// application defining the key. An ExtensionData implementation must
// check to see if the given key is familar to it, and if so return a
// suitable value for the key.
//
// If the given key is unrecognized, the ExtensionData function must
// return a nil interface. (Importantly, not an interface containing a nil
// pointer of some other type.)
// The common implementation of ExtensionData is a single switch statement
// over "key" which has a default case returning nil.
//
// The meaning of any given key is entirely up to the application that
// defines it. Applications consuming ExtensionData from capsule types
// should do so defensively: if the result of ExtensionData is not valid,
// prefer to ignore it or gracefully produce an error rather than causing
// a panic.
ExtensionData func(key interface{}) interface{}
}
// noCapsuleOps is a pointer to a CapsuleOps with no functions set, which
// is used as the default operations value when a type is created using
// the Capsule function.
var noCapsuleOps = &CapsuleOps{}
func (ops *CapsuleOps) assertValid() {
if ops.RawEquals == nil && ops.Equals != nil {
panic("Equals cannot be set without RawEquals")
}
}
// CapsuleOps returns a pointer to the CapsuleOps value for a capsule type,
// or panics if the receiver is not a capsule type.
//
// The caller must not modify the CapsuleOps.
func (ty Type) CapsuleOps() *CapsuleOps {
if !ty.IsCapsuleType() {
panic("not a capsule-typed value")
}
return ty.typeImpl.(*capsuleType).Ops
}
// CapsuleExtensionData is a convenience interface to the ExtensionData
// function that can be optionally implemented for a capsule type. It will
// check to see if the underlying type implements ExtensionData and call it
// if so. If not, it will return nil to indicate that the given key is not
// supported.
//
// See the documentation for CapsuleOps.ExtensionData for more information
// on the purpose of and usage of this mechanism.
//
// If CapsuleExtensionData is called on a non-capsule type then it will panic.
func (ty Type) CapsuleExtensionData(key interface{}) interface{} {
ops := ty.CapsuleOps()
if ops.ExtensionData == nil {
return nil
}
return ops.ExtensionData(key)
}

View File

@ -16,7 +16,19 @@ func getConversion(in cty.Type, out cty.Type, unsafe bool) conversion {
// Wrap the conversion in some standard checks that we don't want to
// have to repeat in every conversion function.
return func(in cty.Value, path cty.Path) (cty.Value, error) {
var ret conversion
ret = func(in cty.Value, path cty.Path) (cty.Value, error) {
if in.IsMarked() {
// We must unmark during the conversion and then re-apply the
// same marks to the result.
in, inMarks := in.Unmark()
v, err := ret(in, path)
if v != cty.NilVal {
v = v.WithMarks(inMarks)
}
return v, err
}
if out == cty.DynamicPseudoType {
// Conversion to DynamicPseudoType always just passes through verbatim.
return in, nil
@ -33,6 +45,8 @@ func getConversion(in cty.Type, out cty.Type, unsafe bool) conversion {
return conv(in, path)
}
return ret
}
func getConversionKnown(in cty.Type, out cty.Type, unsafe bool) conversion {
@ -124,6 +138,30 @@ func getConversionKnown(in cty.Type, out cty.Type, unsafe bool) conversion {
outEty := out.ElementType()
return conversionObjectToMap(in, outEty, unsafe)
case in.IsCapsuleType() || out.IsCapsuleType():
if !unsafe {
// Capsule types can only participate in "unsafe" conversions,
// because we don't know enough about their conversion behaviors
// to be sure that they will always be safe.
return nil
}
if in.Equals(out) {
// conversion to self is never allowed
return nil
}
if out.IsCapsuleType() {
if fn := out.CapsuleOps().ConversionTo; fn != nil {
return conversionToCapsule(in, out, fn)
}
}
if in.IsCapsuleType() {
if fn := in.CapsuleOps().ConversionFrom; fn != nil {
return conversionFromCapsule(in, out, fn)
}
}
// No conversion operation is available, then.
return nil
default:
return nil

View File

@ -0,0 +1,31 @@
package convert
import (
"github.com/zclconf/go-cty/cty"
)
func conversionToCapsule(inTy, outTy cty.Type, fn func(inTy cty.Type) func(cty.Value, cty.Path) (interface{}, error)) conversion {
rawConv := fn(inTy)
if rawConv == nil {
return nil
}
return func(in cty.Value, path cty.Path) (cty.Value, error) {
rawV, err := rawConv(in, path)
if err != nil {
return cty.NilVal, err
}
return cty.CapsuleVal(outTy, rawV), nil
}
}
func conversionFromCapsule(inTy, outTy cty.Type, fn func(outTy cty.Type) func(interface{}, cty.Path) (cty.Value, error)) conversion {
rawConv := fn(outTy)
if rawConv == nil {
return nil
}
return func(in cty.Value, path cty.Path) (cty.Value, error) {
return rawConv(in.EncapsulatedValue(), path)
}
}

View File

@ -23,6 +23,8 @@ type ElementIterator interface {
func canElementIterator(val Value) bool {
switch {
case val.IsMarked():
return false
case val.ty.IsListType():
return true
case val.ty.IsMapType():
@ -39,6 +41,7 @@ func canElementIterator(val Value) bool {
}
func elementIterator(val Value) ElementIterator {
val.assertUnmarked()
switch {
case val.ty.IsListType():
return &listElementIterator{

View File

@ -47,4 +47,24 @@ type Parameter struct {
// values are not, thus improving the type-check accuracy of derived
// values.
AllowDynamicType bool
// If AllowMarked is set then marked values may be passed into this
// argument's slot in the implementation function. If not set, any
// marked value will be unmarked before calling and then the markings
// from that value will be applied automatically to the function result,
// ensuring that the marks get propagated in a simplistic way even if
// a function is unable to handle them.
//
// For any argument whose parameter has AllowMarked set, it's the
// function implementation's responsibility to Unmark the given value
// and propagate the marks appropriatedly to the result in order to
// avoid losing the marks. Application-specific functions might use
// special rules to selectively propagate particular marks.
//
// The automatic unmarking of values applies only to the main
// implementation function. In an application that uses marked values,
// the Type implementation for a function must always be prepared to accept
// marked values, which is easy to achieve by consulting only the type
// and ignoring the value itself.
AllowMarked bool
}

View File

@ -142,6 +142,21 @@ func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error)
for i, spec := range f.spec.Params {
val := posArgs[i]
if val.IsMarked() && !spec.AllowMarked {
// During type checking we just unmark values and discard their
// marks, under the assumption that during actual execution of
// the function we'll do similarly and then re-apply the marks
// afterwards. Note that this does mean that a function that
// inspects values (rather than just types) in its Type
// implementation can potentially fail to take into account marks,
// unless it specifically opts in to seeing them.
unmarked, _ := val.Unmark()
newArgs := make([]cty.Value, len(args))
copy(newArgs, args)
newArgs[i] = unmarked
args = newArgs
}
if val.IsNull() && !spec.AllowNull {
return cty.Type{}, NewArgErrorf(i, "argument must not be null")
}
@ -168,6 +183,15 @@ func (f Function) ReturnTypeForValues(args []cty.Value) (ty cty.Type, err error)
for i, val := range varArgs {
realI := i + len(posArgs)
if val.IsMarked() && !spec.AllowMarked {
// See the similar block in the loop above for what's going on here.
unmarked, _ := val.Unmark()
newArgs := make([]cty.Value, len(args))
copy(newArgs, args)
newArgs[realI] = unmarked
args = newArgs
}
if val.IsNull() && !spec.AllowNull {
return cty.Type{}, NewArgErrorf(realI, "argument must not be null")
}
@ -208,9 +232,10 @@ func (f Function) Call(args []cty.Value) (val cty.Value, err error) {
// Type checking already dealt with most situations relating to our
// parameter specification, but we still need to deal with unknown
// values.
// values and marked values.
posArgs := args[:len(f.spec.Params)]
varArgs := args[len(f.spec.Params):]
var resultMarks []cty.ValueMarks
for i, spec := range f.spec.Params {
val := posArgs[i]
@ -218,14 +243,37 @@ func (f Function) Call(args []cty.Value) (val cty.Value, err error) {
if !val.IsKnown() && !spec.AllowUnknown {
return cty.UnknownVal(expectedType), nil
}
if val.IsMarked() && !spec.AllowMarked {
unwrappedVal, marks := val.Unmark()
// In order to avoid additional overhead on applications that
// are not using marked values, we copy the given args only
// if we encounter a marked value we need to unmark. However,
// as a consequence we end up doing redundant copying if multiple
// marked values need to be unwrapped. That seems okay because
// argument lists are generally small.
newArgs := make([]cty.Value, len(args))
copy(newArgs, args)
newArgs[i] = unwrappedVal
resultMarks = append(resultMarks, marks)
args = newArgs
}
}
if f.spec.VarParam != nil {
spec := f.spec.VarParam
for _, val := range varArgs {
for i, val := range varArgs {
if !val.IsKnown() && !spec.AllowUnknown {
return cty.UnknownVal(expectedType), nil
}
if val.IsMarked() && !spec.AllowMarked {
unwrappedVal, marks := val.Unmark()
newArgs := make([]cty.Value, len(args))
copy(newArgs, args)
newArgs[len(posArgs)+i] = unwrappedVal
resultMarks = append(resultMarks, marks)
args = newArgs
}
}
}
@ -244,6 +292,9 @@ func (f Function) Call(args []cty.Value) (val cty.Value, err error) {
if err != nil {
return cty.NilVal, err
}
if len(resultMarks) > 0 {
retVal = retVal.WithMarks(resultMarks...)
}
}
// Returned value must conform to what the Type function expected, to

View File

@ -11,6 +11,7 @@ var NotFunc = function.New(&function.Spec{
Name: "val",
Type: cty.Bool,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
@ -25,11 +26,13 @@ var AndFunc = function.New(&function.Spec{
Name: "a",
Type: cty.Bool,
AllowDynamicType: true,
AllowMarked: true,
},
{
Name: "b",
Type: cty.Bool,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
@ -44,11 +47,13 @@ var OrFunc = function.New(&function.Spec{
Name: "a",
Type: cty.Bool,
AllowDynamicType: true,
AllowMarked: true,
},
{
Name: "b",
Type: cty.Bool,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),

View File

@ -14,6 +14,7 @@ var AbsoluteFunc = function.New(&function.Spec{
Name: "num",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Number),
@ -196,11 +197,13 @@ var GreaterThanFunc = function.New(&function.Spec{
Name: "a",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
{
Name: "b",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
@ -215,11 +218,13 @@ var GreaterThanOrEqualToFunc = function.New(&function.Spec{
Name: "a",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
{
Name: "b",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
@ -234,11 +239,13 @@ var LessThanFunc = function.New(&function.Spec{
Name: "a",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
{
Name: "b",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
@ -253,11 +260,13 @@ var LessThanOrEqualToFunc = function.New(&function.Spec{
Name: "a",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
{
Name: "b",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
@ -272,6 +281,7 @@ var NegateFunc = function.New(&function.Spec{
Name: "num",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Number),

View File

@ -3,10 +3,10 @@ package stdlib
import (
"strings"
"github.com/apparentlymart/go-textseg/textseg"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/gocty"
"github.com/apparentlymart/go-textseg/textseg"
)
var UpperFunc = function.New(&function.Spec{

View File

@ -3,6 +3,7 @@ package cty
import (
"bytes"
"encoding/gob"
"errors"
"fmt"
"math/big"
@ -15,6 +16,10 @@ import (
// Currently it is not possible to represent values of capsule types in gob,
// because the types themselves cannot be represented.
func (val Value) GobEncode() ([]byte, error) {
if val.IsMarked() {
return nil, errors.New("value is marked")
}
buf := &bytes.Buffer{}
enc := gob.NewEncoder(buf)

View File

@ -9,6 +9,10 @@ import (
)
func marshal(val cty.Value, t cty.Type, path cty.Path, b *bytes.Buffer) error {
if val.IsMarked() {
return path.NewErrorf("value has marks, so it cannot be seralized")
}
// If we're going to decode as DynamicPseudoType then we need to save
// dynamic type information to recover the real type.
if t == cty.DynamicPseudoType && val.Type() != cty.DynamicPseudoType {

296
vendor/github.com/zclconf/go-cty/cty/marks.go generated vendored Normal file
View File

@ -0,0 +1,296 @@
package cty
import (
"fmt"
"strings"
)
// marker is an internal wrapper type used to add special "marks" to values.
//
// A "mark" is an annotation that can be used to represent additional
// characteristics of values that propagate through operation methods to
// result values. However, a marked value cannot be used with integration
// methods normally associated with its type, in order to ensure that
// calling applications don't inadvertently drop marks as they round-trip
// values out of cty and back in again.
//
// Marked values are created only explicitly by the calling application, so
// an application that never marks a value does not need to worry about
// encountering marked values.
type marker struct {
realV interface{}
marks ValueMarks
}
// ValueMarks is a map, representing a set, of "mark" values associated with
// a Value. See Value.Mark for more information on the usage of mark values.
type ValueMarks map[interface{}]struct{}
// NewValueMarks constructs a new ValueMarks set with the given mark values.
func NewValueMarks(marks ...interface{}) ValueMarks {
if len(marks) == 0 {
return nil
}
ret := make(ValueMarks, len(marks))
for _, v := range marks {
ret[v] = struct{}{}
}
return ret
}
// Equal returns true if the receiver and the given ValueMarks both contain
// the same marks.
func (m ValueMarks) Equal(o ValueMarks) bool {
if len(m) != len(o) {
return false
}
for v := range m {
if _, ok := o[v]; !ok {
return false
}
}
return true
}
func (m ValueMarks) GoString() string {
var s strings.Builder
s.WriteString("cty.NewValueMarks(")
i := 0
for mv := range m {
if i != 0 {
s.WriteString(", ")
}
s.WriteString(fmt.Sprintf("%#v", mv))
i++
}
s.WriteString(")")
return s.String()
}
// IsMarked returns true if and only if the receiving value carries at least
// one mark. A marked value cannot be used directly with integration methods
// without explicitly unmarking it (and retrieving the markings) first.
func (val Value) IsMarked() bool {
_, ok := val.v.(marker)
return ok
}
// HasMark returns true if and only if the receiving value has the given mark.
func (val Value) HasMark(mark interface{}) bool {
if mr, ok := val.v.(marker); ok {
_, ok := mr.marks[mark]
return ok
}
return false
}
// ContainsMarked returns true if the receiving value or any value within it
// is marked.
//
// This operation is relatively expensive. If you only need a shallow result,
// use IsMarked instead.
func (val Value) ContainsMarked() bool {
ret := false
Walk(val, func(_ Path, v Value) (bool, error) {
if v.IsMarked() {
ret = true
return false, nil
}
return true, nil
})
return ret
}
func (val Value) assertUnmarked() {
if val.IsMarked() {
panic("value is marked, so must be unmarked first")
}
}
// Marks returns a map (representing a set) of all of the mark values
// associated with the receiving value, without changing the marks. Returns nil
// if the value is not marked at all.
func (val Value) Marks() ValueMarks {
if mr, ok := val.v.(marker); ok {
// copy so that the caller can't mutate our internals
ret := make(ValueMarks, len(mr.marks))
for k, v := range mr.marks {
ret[k] = v
}
return ret
}
return nil
}
// HasSameMarks returns true if an only if the receiver and the given other
// value have identical marks.
func (val Value) HasSameMarks(other Value) bool {
vm, vmOK := val.v.(marker)
om, omOK := other.v.(marker)
if vmOK != omOK {
return false
}
if vmOK {
return vm.marks.Equal(om.marks)
}
return true
}
// Mark returns a new value that as the same type and underlying value as
// the receiver but that also carries the given value as a "mark".
//
// Marks are used to carry additional application-specific characteristics
// associated with values. A marked value can be used with operation methods,
// in which case the marks are propagated to the operation results. A marked
// value _cannot_ be used with integration methods, so callers of those
// must derive an unmarked value using Unmark (and thus explicitly handle
// the markings) before calling the integration methods.
//
// The mark value can be any value that would be valid to use as a map key.
// The mark value should be of a named type in order to use the type itself
// as a namespace for markings. That type can be unexported if desired, in
// order to ensure that the mark can only be handled through the defining
// package's own functions.
//
// An application that never calls this method does not need to worry about
// handling marked values.
func (val Value) Mark(mark interface{}) Value {
var newMarker marker
newMarker.realV = val.v
if mr, ok := val.v.(marker); ok {
// It's already a marker, so we'll retain existing marks.
newMarker.marks = make(ValueMarks, len(mr.marks)+1)
for k, v := range mr.marks {
newMarker.marks[k] = v
}
} else {
// It's not a marker yet, so we're creating the first mark.
newMarker.marks = make(ValueMarks, 1)
}
newMarker.marks[mark] = struct{}{}
return Value{
ty: val.ty,
v: newMarker,
}
}
// Unmark separates the marks of the receiving value from the value itself,
// removing a new unmarked value and a map (representing a set) of the marks.
//
// If the receiver isn't marked, Unmark returns it verbatim along with a nil
// map of marks.
func (val Value) Unmark() (Value, ValueMarks) {
if !val.IsMarked() {
return val, nil
}
mr := val.v.(marker)
marks := val.Marks() // copy so that the caller can't mutate our internals
return Value{
ty: val.ty,
v: mr.realV,
}, marks
}
// UnmarkDeep is similar to Unmark, but it works with an entire nested structure
// rather than just the given value directly.
//
// The result is guaranteed to contain no nested values that are marked, and
// the returned marks set includes the superset of all of the marks encountered
// during the operation.
func (val Value) UnmarkDeep() (Value, ValueMarks) {
marks := make(ValueMarks)
ret, _ := Transform(val, func(_ Path, v Value) (Value, error) {
unmarkedV, valueMarks := v.Unmark()
for m, s := range valueMarks {
marks[m] = s
}
return unmarkedV, nil
})
return ret, marks
}
func (val Value) unmarkForce() Value {
unw, _ := val.Unmark()
return unw
}
// WithMarks returns a new value that has the same type and underlying value
// as the receiver and also has the marks from the given maps (representing
// sets).
func (val Value) WithMarks(marks ...ValueMarks) Value {
if len(marks) == 0 {
return val
}
ownMarks := val.Marks()
markCount := len(ownMarks)
for _, s := range marks {
markCount += len(s)
}
if markCount == 0 {
return val
}
newMarks := make(ValueMarks, markCount)
for m := range ownMarks {
newMarks[m] = struct{}{}
}
for _, s := range marks {
for m := range s {
newMarks[m] = struct{}{}
}
}
v := val.v
if mr, ok := v.(marker); ok {
v = mr.realV
}
return Value{
ty: val.ty,
v: marker{
realV: v,
marks: newMarks,
},
}
}
// WithSameMarks returns a new value that has the same type and underlying
// value as the receiver and also has the marks from the given source values.
//
// Use this if you are implementing your own higher-level operations against
// cty using the integration methods, to re-introduce the marks from the
// source values of the operation.
func (val Value) WithSameMarks(srcs ...Value) Value {
if len(srcs) == 0 {
return val
}
ownMarks := val.Marks()
markCount := len(ownMarks)
for _, sv := range srcs {
if mr, ok := sv.v.(marker); ok {
markCount += len(mr.marks)
}
}
if markCount == 0 {
return val
}
newMarks := make(ValueMarks, markCount)
for m := range ownMarks {
newMarks[m] = struct{}{}
}
for _, sv := range srcs {
if mr, ok := sv.v.(marker); ok {
for m := range mr.marks {
newMarks[m] = struct{}{}
}
}
}
v := val.v
if mr, ok := v.(marker); ok {
v = mr.realV
}
return Value{
ty: val.ty,
v: marker{
realV: v,
marks: newMarks,
},
}
}

View File

@ -41,6 +41,10 @@ func Marshal(val cty.Value, ty cty.Type) ([]byte, error) {
}
func marshal(val cty.Value, ty cty.Type, path cty.Path, enc *msgpack.Encoder) error {
if val.IsMarked() {
return path.NewErrorf("value has marks, so it cannot be seralized")
}
// If we're going to decode as DynamicPseudoType then we need to save
// dynamic type information to recover the real type.
if ty == cty.DynamicPseudoType && val.Type() != cty.DynamicPseudoType {

View File

@ -119,7 +119,13 @@ func (s ValueSet) SymmetricDifference(other ValueSet) ValueSet {
}
// requireElementType panics if the given value is not of the set's element type.
//
// It also panics if the given value is marked, because marked values cannot
// be stored in sets.
func (s ValueSet) requireElementType(v Value) {
if v.IsMarked() {
panic("cannot store marked value directly in a set (make the set itself unknown instead)")
}
if !v.Type().Equals(s.ElementType()) {
panic(fmt.Errorf("attempt to use %#v value with set of %#v", v.Type(), s.ElementType()))
}

View File

@ -32,7 +32,10 @@ var _ set.OrderedRules = setRules{}
// This function is not safe to use for security-related applications, since
// the hash used is not strong enough.
func (val Value) Hash() int {
hashBytes := makeSetHashBytes(val)
hashBytes, marks := makeSetHashBytes(val)
if len(marks) > 0 {
panic("can't take hash of value that has marks or has embedded values that have marks")
}
return int(crc32.ChecksumIEEE(hashBytes))
}
@ -110,19 +113,20 @@ func (r setRules) Less(v1, v2 interface{}) bool {
// default consistent-but-undefined ordering then. This situation is
// not considered a compatibility constraint; callers should rely only
// on the ordering rules for primitive values.
v1h := makeSetHashBytes(v1v)
v2h := makeSetHashBytes(v2v)
v1h, _ := makeSetHashBytes(v1v)
v2h, _ := makeSetHashBytes(v2v)
return bytes.Compare(v1h, v2h) < 0
}
}
func makeSetHashBytes(val Value) []byte {
func makeSetHashBytes(val Value) ([]byte, ValueMarks) {
var buf bytes.Buffer
appendSetHashBytes(val, &buf)
return buf.Bytes()
marks := make(ValueMarks)
appendSetHashBytes(val, &buf, marks)
return buf.Bytes(), marks
}
func appendSetHashBytes(val Value, buf *bytes.Buffer) {
func appendSetHashBytes(val Value, buf *bytes.Buffer, marks ValueMarks) {
// Exactly what bytes we generate here don't matter as long as the following
// constraints hold:
// - Unknown and null values all generate distinct strings from
@ -136,6 +140,19 @@ func appendSetHashBytes(val Value, buf *bytes.Buffer) {
// the Equivalent function will still distinguish values, but set
// performance will be best if we are able to produce a distinct string
// for each distinct value, unknown values notwithstanding.
// Marks aren't considered part of a value for equality-testing purposes,
// so we'll unmark our value before we work with it but we'll remember
// the marks in case the caller needs to re-apply them to a derived
// value.
if val.IsMarked() {
unmarkedVal, valMarks := val.Unmark()
for m := range valMarks {
marks[m] = struct{}{}
}
val = unmarkedVal
}
if !val.IsKnown() {
buf.WriteRune('?')
return
@ -175,9 +192,9 @@ func appendSetHashBytes(val Value, buf *bytes.Buffer) {
if val.ty.IsMapType() {
buf.WriteRune('{')
val.ForEachElement(func(keyVal, elementVal Value) bool {
appendSetHashBytes(keyVal, buf)
appendSetHashBytes(keyVal, buf, marks)
buf.WriteRune(':')
appendSetHashBytes(elementVal, buf)
appendSetHashBytes(elementVal, buf, marks)
buf.WriteRune(';')
return false
})
@ -188,7 +205,7 @@ func appendSetHashBytes(val Value, buf *bytes.Buffer) {
if val.ty.IsListType() || val.ty.IsSetType() {
buf.WriteRune('[')
val.ForEachElement(func(keyVal, elementVal Value) bool {
appendSetHashBytes(elementVal, buf)
appendSetHashBytes(elementVal, buf, marks)
buf.WriteRune(';')
return false
})
@ -204,7 +221,7 @@ func appendSetHashBytes(val Value, buf *bytes.Buffer) {
}
sort.Strings(attrNames)
for _, attrName := range attrNames {
appendSetHashBytes(val.GetAttr(attrName), buf)
appendSetHashBytes(val.GetAttr(attrName), buf, marks)
buf.WriteRune(';')
}
buf.WriteRune('>')
@ -214,7 +231,7 @@ func appendSetHashBytes(val Value, buf *bytes.Buffer) {
if val.ty.IsTupleType() {
buf.WriteRune('<')
val.ForEachElement(func(keyVal, elementVal Value) bool {
appendSetHashBytes(elementVal, buf)
appendSetHashBytes(elementVal, buf, marks)
buf.WriteRune(';')
return false
})

View File

@ -45,6 +45,9 @@ func (val Value) Type() Type {
// operating on other unknown values, and so an application that never
// introduces Unknown values can be guaranteed to never receive any either.
func (val Value) IsKnown() bool {
if val.IsMarked() {
return val.unmarkForce().IsKnown()
}
return val.v != unknown
}
@ -53,6 +56,9 @@ func (val Value) IsKnown() bool {
// produces null, so an application that never introduces Null values can
// be guaranteed to never receive any either.
func (val Value) IsNull() bool {
if val.IsMarked() {
return val.unmarkForce().IsNull()
}
return val.v == nil
}
@ -74,6 +80,10 @@ var NilVal = Value{
// inside collections and structures to see if there are any nested unknown
// values.
func (val Value) IsWhollyKnown() bool {
if val.IsMarked() {
return val.unmarkForce().IsWhollyKnown()
}
if !val.IsKnown() {
return false
}

View File

@ -240,8 +240,18 @@ func SetVal(vals []Value) Value {
}
elementType := DynamicPseudoType
rawList := make([]interface{}, len(vals))
var markSets []ValueMarks
for i, val := range vals {
if unmarkedVal, marks := val.UnmarkDeep(); len(marks) > 0 {
val = unmarkedVal
markSets = append(markSets, marks)
}
if val.ContainsMarked() {
// FIXME: Allow this, but unmark the values and apply the
// marking to the set itself instead.
panic("set cannot contain marked values")
}
if elementType == DynamicPseudoType {
elementType = val.ty
} else if val.ty != DynamicPseudoType && !elementType.Equals(val.ty) {
@ -259,7 +269,7 @@ func SetVal(vals []Value) Value {
return Value{
ty: Set(elementType),
v: rawVal,
}
}.WithMarks(markSets...)
}
// SetValFromValueSet returns a Value of set type based on an already-constructed

View File

@ -11,6 +11,18 @@ import (
// GoString is an implementation of fmt.GoStringer that produces concise
// source-like representations of values suitable for use in debug messages.
func (val Value) GoString() string {
if val.IsMarked() {
unVal, marks := val.Unmark()
if len(marks) == 1 {
var mark interface{}
for m := range marks {
mark = m
}
return fmt.Sprintf("%#v.Mark(%#v)", unVal, mark)
}
return fmt.Sprintf("%#v.WithMarks(%#v)", unVal, marks)
}
if val == NilVal {
return "cty.NilVal"
}
@ -82,7 +94,11 @@ func (val Value) GoString() string {
vals := val.AsValueMap()
return fmt.Sprintf("cty.ObjectVal(%#v)", vals)
case val.ty.IsCapsuleType():
return fmt.Sprintf("cty.CapsuleVal(%#v, %#v)", val.ty, val.v)
impl := val.ty.CapsuleOps().GoString
if impl == nil {
return fmt.Sprintf("cty.CapsuleVal(%#v, %#v)", val.ty, val.v)
}
return impl(val.EncapsulatedValue())
}
// Default exposes implementation details, so should actually cover
@ -101,6 +117,12 @@ func (val Value) GoString() string {
// Use RawEquals to compare if two values are equal *ignoring* the
// short-circuit rules and the exception for null values.
func (val Value) Equals(other Value) Value {
if val.IsMarked() || other.IsMarked() {
val, valMarks := val.Unmark()
other, otherMarks := other.Unmark()
return val.Equals(other).WithMarks(valMarks, otherMarks)
}
// Start by handling Unknown values before considering types.
// This needs to be done since Null values are always equal regardless of
// type.
@ -288,10 +310,22 @@ func (val Value) Equals(other Value) Value {
}
}
case ty.IsCapsuleType():
// A capsule type's encapsulated value is a pointer to a value of its
// native type, so we can just compare these to get the identity test
// we need.
return BoolVal(val.v == other.v)
impl := val.ty.CapsuleOps().Equals
if impl == nil {
impl := val.ty.CapsuleOps().RawEquals
if impl == nil {
// A capsule type's encapsulated value is a pointer to a value of its
// native type, so we can just compare these to get the identity test
// we need.
return BoolVal(val.v == other.v)
}
return BoolVal(impl(val.v, other.v))
}
ret := impl(val.v, other.v)
if !ret.Type().Equals(Bool) {
panic(fmt.Sprintf("Equals for %#v returned %#v, not cty.Bool", ty, ret.Type()))
}
return ret
default:
// should never happen
@ -314,6 +348,7 @@ func (val Value) NotEqual(other Value) Value {
// or null values. For more robust handling with unknown value
// short-circuiting, use val.Equals(cty.True).
func (val Value) True() bool {
val.assertUnmarked()
if val.ty != Bool {
panic("not bool")
}
@ -338,6 +373,13 @@ func (val Value) RawEquals(other Value) bool {
if !val.ty.Equals(other.ty) {
return false
}
if !val.HasSameMarks(other) {
return false
}
// Since we've now checked the marks, we'll unmark for the rest of this...
val = val.unmarkForce()
other = other.unmarkForce()
if (!val.IsKnown()) && (!other.IsKnown()) {
return true
}
@ -448,10 +490,14 @@ func (val Value) RawEquals(other Value) bool {
}
return false
case ty.IsCapsuleType():
// A capsule type's encapsulated value is a pointer to a value of its
// native type, so we can just compare these to get the identity test
// we need.
return val.v == other.v
impl := val.ty.CapsuleOps().RawEquals
if impl == nil {
// A capsule type's encapsulated value is a pointer to a value of its
// native type, so we can just compare these to get the identity test
// we need.
return val.v == other.v
}
return impl(val.v, other.v)
default:
// should never happen
@ -462,6 +508,12 @@ func (val Value) RawEquals(other Value) bool {
// Add returns the sum of the receiver and the given other value. Both values
// must be numbers; this method will panic if not.
func (val Value) Add(other Value) Value {
if val.IsMarked() || other.IsMarked() {
val, valMarks := val.Unmark()
other, otherMarks := other.Unmark()
return val.Add(other).WithMarks(valMarks, otherMarks)
}
if shortCircuit := mustTypeCheck(Number, Number, val, other); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Number)
return *shortCircuit
@ -475,6 +527,12 @@ func (val Value) Add(other Value) Value {
// Subtract returns receiver minus the given other value. Both values must be
// numbers; this method will panic if not.
func (val Value) Subtract(other Value) Value {
if val.IsMarked() || other.IsMarked() {
val, valMarks := val.Unmark()
other, otherMarks := other.Unmark()
return val.Subtract(other).WithMarks(valMarks, otherMarks)
}
if shortCircuit := mustTypeCheck(Number, Number, val, other); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Number)
return *shortCircuit
@ -486,6 +544,11 @@ func (val Value) Subtract(other Value) Value {
// Negate returns the numeric negative of the receiver, which must be a number.
// This method will panic when given a value of any other type.
func (val Value) Negate() Value {
if val.IsMarked() {
val, valMarks := val.Unmark()
return val.Negate().WithMarks(valMarks)
}
if shortCircuit := mustTypeCheck(Number, Number, val); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Number)
return *shortCircuit
@ -498,6 +561,12 @@ func (val Value) Negate() Value {
// Multiply returns the product of the receiver and the given other value.
// Both values must be numbers; this method will panic if not.
func (val Value) Multiply(other Value) Value {
if val.IsMarked() || other.IsMarked() {
val, valMarks := val.Unmark()
other, otherMarks := other.Unmark()
return val.Multiply(other).WithMarks(valMarks, otherMarks)
}
if shortCircuit := mustTypeCheck(Number, Number, val, other); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Number)
return *shortCircuit
@ -520,6 +589,12 @@ func (val Value) Multiply(other Value) Value {
// If both values are zero or infinity, this function will panic with
// an instance of big.ErrNaN.
func (val Value) Divide(other Value) Value {
if val.IsMarked() || other.IsMarked() {
val, valMarks := val.Unmark()
other, otherMarks := other.Unmark()
return val.Divide(other).WithMarks(valMarks, otherMarks)
}
if shortCircuit := mustTypeCheck(Number, Number, val, other); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Number)
return *shortCircuit
@ -546,6 +621,12 @@ func (val Value) Divide(other Value) Value {
// may wish to disallow such things outright or implement their own modulo
// if they disagree with the interpretation used here.
func (val Value) Modulo(other Value) Value {
if val.IsMarked() || other.IsMarked() {
val, valMarks := val.Unmark()
other, otherMarks := other.Unmark()
return val.Modulo(other).WithMarks(valMarks, otherMarks)
}
if shortCircuit := mustTypeCheck(Number, Number, val, other); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Number)
return *shortCircuit
@ -576,6 +657,11 @@ func (val Value) Modulo(other Value) Value {
// Absolute returns the absolute (signless) value of the receiver, which must
// be a number or this method will panic.
func (val Value) Absolute() Value {
if val.IsMarked() {
val, valMarks := val.Unmark()
return val.Absolute().WithMarks(valMarks)
}
if shortCircuit := mustTypeCheck(Number, Number, val); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Number)
return *shortCircuit
@ -596,6 +682,11 @@ func (val Value) Absolute() Value {
// This method may be called on a value whose type is DynamicPseudoType,
// in which case the result will also be DynamicVal.
func (val Value) GetAttr(name string) Value {
if val.IsMarked() {
val, valMarks := val.Unmark()
return val.GetAttr(name).WithMarks(valMarks)
}
if val.ty == DynamicPseudoType {
return DynamicVal
}
@ -638,6 +729,12 @@ func (val Value) GetAttr(name string) Value {
// This method may be called on a value whose type is DynamicPseudoType,
// in which case the result will also be the DynamicValue.
func (val Value) Index(key Value) Value {
if val.IsMarked() || key.IsMarked() {
val, valMarks := val.Unmark()
key, keyMarks := key.Unmark()
return val.Index(key).WithMarks(valMarks, keyMarks)
}
if val.ty == DynamicPseudoType {
return DynamicVal
}
@ -733,6 +830,12 @@ func (val Value) Index(key Value) Value {
// This method will panic if the receiver is not indexable, but does not
// impose any panic-causing type constraints on the key.
func (val Value) HasIndex(key Value) Value {
if val.IsMarked() || key.IsMarked() {
val, valMarks := val.Unmark()
key, keyMarks := key.Unmark()
return val.HasIndex(key).WithMarks(valMarks, keyMarks)
}
if val.ty == DynamicPseudoType {
return UnknownVal(Bool)
}
@ -810,6 +913,12 @@ func (val Value) HasIndex(key Value) Value {
//
// This method will panic if the receiver is not a set, or if it is a null set.
func (val Value) HasElement(elem Value) Value {
if val.IsMarked() || elem.IsMarked() {
val, valMarks := val.Unmark()
elem, elemMarks := elem.Unmark()
return val.HasElement(elem).WithMarks(valMarks, elemMarks)
}
ty := val.Type()
if !ty.IsSetType() {
@ -841,6 +950,11 @@ func (val Value) HasElement(elem Value) Value {
// of a string, call AsString and take the length of the native Go string
// that is returned.
func (val Value) Length() Value {
if val.IsMarked() {
val, valMarks := val.Unmark()
return val.Length().WithMarks(valMarks)
}
if val.Type().IsTupleType() {
// For tuples, we can return the length even if the value is not known.
return NumberIntVal(int64(val.Type().Length()))
@ -859,6 +973,7 @@ func (val Value) Length() Value {
// This is an integration method provided for the convenience of code bridging
// into Go's type system.
func (val Value) LengthInt() int {
val.assertUnmarked()
if val.Type().IsTupleType() {
// For tuples, we can return the length even if the value is not known.
return val.Type().Length()
@ -915,6 +1030,7 @@ func (val Value) LengthInt() int {
// ElementIterator is an integration method, so it cannot handle Unknown
// values. This method will panic if the receiver is Unknown.
func (val Value) ElementIterator() ElementIterator {
val.assertUnmarked()
if !val.IsKnown() {
panic("can't use ElementIterator on unknown value")
}
@ -943,6 +1059,7 @@ func (val Value) CanIterateElements() bool {
// ForEachElement is an integration method, so it cannot handle Unknown
// values. This method will panic if the receiver is Unknown.
func (val Value) ForEachElement(cb ElementCallback) bool {
val.assertUnmarked()
it := val.ElementIterator()
for it.Next() {
key, val := it.Element()
@ -957,6 +1074,11 @@ func (val Value) ForEachElement(cb ElementCallback) bool {
// Not returns the logical inverse of the receiver, which must be of type
// Bool or this method will panic.
func (val Value) Not() Value {
if val.IsMarked() {
val, valMarks := val.Unmark()
return val.Not().WithMarks(valMarks)
}
if shortCircuit := mustTypeCheck(Bool, Bool, val); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Bool)
return *shortCircuit
@ -968,6 +1090,12 @@ func (val Value) Not() Value {
// And returns the result of logical AND with the receiver and the other given
// value, which must both be of type Bool or this method will panic.
func (val Value) And(other Value) Value {
if val.IsMarked() || other.IsMarked() {
val, valMarks := val.Unmark()
other, otherMarks := other.Unmark()
return val.And(other).WithMarks(valMarks, otherMarks)
}
if shortCircuit := mustTypeCheck(Bool, Bool, val, other); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Bool)
return *shortCircuit
@ -979,6 +1107,12 @@ func (val Value) And(other Value) Value {
// Or returns the result of logical OR with the receiver and the other given
// value, which must both be of type Bool or this method will panic.
func (val Value) Or(other Value) Value {
if val.IsMarked() || other.IsMarked() {
val, valMarks := val.Unmark()
other, otherMarks := other.Unmark()
return val.Or(other).WithMarks(valMarks, otherMarks)
}
if shortCircuit := mustTypeCheck(Bool, Bool, val, other); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Bool)
return *shortCircuit
@ -990,6 +1124,12 @@ func (val Value) Or(other Value) Value {
// LessThan returns True if the receiver is less than the other given value,
// which must both be numbers or this method will panic.
func (val Value) LessThan(other Value) Value {
if val.IsMarked() || other.IsMarked() {
val, valMarks := val.Unmark()
other, otherMarks := other.Unmark()
return val.LessThan(other).WithMarks(valMarks, otherMarks)
}
if shortCircuit := mustTypeCheck(Number, Bool, val, other); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Bool)
return *shortCircuit
@ -1001,6 +1141,12 @@ func (val Value) LessThan(other Value) Value {
// GreaterThan returns True if the receiver is greater than the other given
// value, which must both be numbers or this method will panic.
func (val Value) GreaterThan(other Value) Value {
if val.IsMarked() || other.IsMarked() {
val, valMarks := val.Unmark()
other, otherMarks := other.Unmark()
return val.GreaterThan(other).WithMarks(valMarks, otherMarks)
}
if shortCircuit := mustTypeCheck(Number, Bool, val, other); shortCircuit != nil {
shortCircuit = forceShortCircuitType(shortCircuit, Bool)
return *shortCircuit
@ -1022,6 +1168,7 @@ func (val Value) GreaterThanOrEqualTo(other Value) Value {
// AsString returns the native string from a non-null, non-unknown cty.String
// value, or panics if called on any other value.
func (val Value) AsString() string {
val.assertUnmarked()
if val.ty != String {
panic("not a string")
}
@ -1041,6 +1188,7 @@ func (val Value) AsString() string {
// For more convenient conversions to other native numeric types, use the
// "gocty" package.
func (val Value) AsBigFloat() *big.Float {
val.assertUnmarked()
if val.ty != Number {
panic("not a number")
}
@ -1064,6 +1212,7 @@ func (val Value) AsBigFloat() *big.Float {
// For more convenient conversions to slices of more specific types, use
// the "gocty" package.
func (val Value) AsValueSlice() []Value {
val.assertUnmarked()
l := val.LengthInt()
if l == 0 {
return nil
@ -1084,6 +1233,7 @@ func (val Value) AsValueSlice() []Value {
// For more convenient conversions to maps of more specific types, use
// the "gocty" package.
func (val Value) AsValueMap() map[string]Value {
val.assertUnmarked()
l := val.LengthInt()
if l == 0 {
return nil
@ -1108,6 +1258,7 @@ func (val Value) AsValueMap() map[string]Value {
//
// The returned ValueSet can store only values of the receiver's element type.
func (val Value) AsValueSet() ValueSet {
val.assertUnmarked()
if !val.Type().IsCollectionType() {
panic("not a collection type")
}
@ -1130,6 +1281,7 @@ func (val Value) AsValueSet() ValueSet {
// the value. Since cty considers values to be immutable, it is strongly
// recommended to treat the encapsulated value itself as immutable too.
func (val Value) EncapsulatedValue() interface{} {
val.assertUnmarked()
if !val.Type().IsCapsuleType() {
panic("not a capsule-typed value")
}

5
vendor/modules.txt vendored
View File

@ -349,7 +349,7 @@ github.com/hashicorp/hcl/hcl/scanner
github.com/hashicorp/hcl/hcl/strconv
github.com/hashicorp/hcl/json/scanner
github.com/hashicorp/hcl/json/token
# github.com/hashicorp/hcl/v2 v2.2.0
# github.com/hashicorp/hcl/v2 v2.3.0
github.com/hashicorp/hcl/v2
github.com/hashicorp/hcl/v2/hclsyntax
github.com/hashicorp/hcl/v2/hcldec
@ -360,6 +360,7 @@ github.com/hashicorp/hcl/v2/hclparse
github.com/hashicorp/hcl/v2/gohcl
github.com/hashicorp/hcl/v2/ext/typeexpr
github.com/hashicorp/hcl/v2/ext/dynblock
github.com/hashicorp/hcl/v2/ext/customdecode
github.com/hashicorp/hcl/v2/hcltest
# github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590
github.com/hashicorp/hil
@ -484,7 +485,7 @@ github.com/vmihailenco/msgpack/codes
github.com/xanzy/ssh-agent
# github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557
github.com/xlab/treeprint
# github.com/zclconf/go-cty v1.1.1
# github.com/zclconf/go-cty v1.2.1
github.com/zclconf/go-cty/cty
github.com/zclconf/go-cty/cty/gocty
github.com/zclconf/go-cty/cty/convert