mirror of https://github.com/hashicorp/terraform
This is largely minor bugfixes for issues found since we last updated the vendoring. There are some new features here that Terraform is not yet using and thus present little risk. In particular this includes the HCL-JSON spec change where arrays can now be used at any level in a block label structure, to allow for preserving the relative order of blocks.pull/17533/head
parent
a61b40c7a2
commit
59a49c6b3f
@ -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.
|
||||
@ -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
|
||||
@ -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(),
|
||||
}}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
package typeexpr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// Type attempts to process the given expression as a type expression and, if
|
||||
// successful, returns the resulting type. If unsuccessful, error diagnostics
|
||||
// are returned.
|
||||
func Type(expr hcl.Expression) (cty.Type, hcl.Diagnostics) {
|
||||
return getType(expr, false)
|
||||
}
|
||||
|
||||
// TypeConstraint attempts to parse the given expression as a type constraint
|
||||
// and, if successful, returns the resulting type. If unsuccessful, error
|
||||
// diagnostics are returned.
|
||||
//
|
||||
// A type constraint has the same structure as a type, but it additionally
|
||||
// allows the keyword "any" to represent cty.DynamicPseudoType, which is often
|
||||
// used as a wildcard in type checking and type conversion operations.
|
||||
func TypeConstraint(expr hcl.Expression) (cty.Type, hcl.Diagnostics) {
|
||||
return getType(expr, true)
|
||||
}
|
||||
|
||||
// TypeString returns a string rendering of the given type as it would be
|
||||
// expected to appear in the HCL native syntax.
|
||||
//
|
||||
// This is primarily intended for showing types to the user in an application
|
||||
// that uses typexpr, where the user can be assumed to be familiar with the
|
||||
// type expression syntax. In applications that do not use typeexpr these
|
||||
// results may be confusing to the user and so type.FriendlyName may be
|
||||
// preferable, even though it's less precise.
|
||||
//
|
||||
// TypeString produces reasonable results only for types like what would be
|
||||
// produced by the Type and TypeConstraint functions. In particular, it cannot
|
||||
// support capsule types.
|
||||
func TypeString(ty cty.Type) string {
|
||||
// Easy cases first
|
||||
switch ty {
|
||||
case cty.String:
|
||||
return "string"
|
||||
case cty.Bool:
|
||||
return "bool"
|
||||
case cty.Number:
|
||||
return "number"
|
||||
case cty.DynamicPseudoType:
|
||||
return "any"
|
||||
}
|
||||
|
||||
if ty.IsCapsuleType() {
|
||||
panic("TypeString does not support capsule types")
|
||||
}
|
||||
|
||||
if ty.IsCollectionType() {
|
||||
ety := ty.ElementType()
|
||||
etyString := TypeString(ety)
|
||||
switch {
|
||||
case ty.IsListType():
|
||||
return fmt.Sprintf("list(%s)", etyString)
|
||||
case ty.IsSetType():
|
||||
return fmt.Sprintf("set(%s)", etyString)
|
||||
case ty.IsMapType():
|
||||
return fmt.Sprintf("map(%s)", etyString)
|
||||
default:
|
||||
// Should never happen because the above is exhaustive
|
||||
panic("unsupported collection type")
|
||||
}
|
||||
}
|
||||
|
||||
if ty.IsObjectType() {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("object({")
|
||||
atys := ty.AttributeTypes()
|
||||
names := make([]string, 0, len(atys))
|
||||
for name := range atys {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
first := true
|
||||
for _, name := range names {
|
||||
aty := atys[name]
|
||||
if !first {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
if !hclsyntax.ValidIdentifier(name) {
|
||||
// Should never happen for any type produced by this package,
|
||||
// but we'll do something reasonable here just so we don't
|
||||
// produce garbage if someone gives us a hand-assembled object
|
||||
// type that has weird attribute names.
|
||||
// Using Go-style quoting here isn't perfect, since it doesn't
|
||||
// exactly match HCL syntax, but it's fine for an edge-case.
|
||||
buf.WriteString(fmt.Sprintf("%q", name))
|
||||
} else {
|
||||
buf.WriteString(name)
|
||||
}
|
||||
buf.WriteByte('=')
|
||||
buf.WriteString(TypeString(aty))
|
||||
first = false
|
||||
}
|
||||
buf.WriteString("})")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
if ty.IsTupleType() {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("tuple([")
|
||||
etys := ty.TupleElementTypes()
|
||||
first := true
|
||||
for _, ety := range etys {
|
||||
if !first {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
buf.WriteString(TypeString(ety))
|
||||
first = false
|
||||
}
|
||||
buf.WriteString("])")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Should never happen because we covered all cases above.
|
||||
panic(fmt.Errorf("unsupported type %#v", ty))
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package hcl
|
||||
|
||||
// ExprCall tests if the given expression is a function call and,
|
||||
// if so, extracts the function name and the expressions that represent
|
||||
// the arguments. If the given expression is not statically a function call,
|
||||
// error diagnostics are returned.
|
||||
//
|
||||
// A particular Expression implementation can support this function by
|
||||
// offering a method called ExprCall that takes no arguments and returns
|
||||
// *StaticCall. This method should return nil if a static call cannot
|
||||
// be extracted. Alternatively, an implementation can support
|
||||
// UnwrapExpression to delegate handling of this function to a wrapped
|
||||
// Expression object.
|
||||
func ExprCall(expr Expression) (*StaticCall, Diagnostics) {
|
||||
type exprCall interface {
|
||||
ExprCall() *StaticCall
|
||||
}
|
||||
|
||||
physExpr := UnwrapExpressionUntil(expr, func(expr Expression) bool {
|
||||
_, supported := expr.(exprCall)
|
||||
return supported
|
||||
})
|
||||
|
||||
if exC, supported := physExpr.(exprCall); supported {
|
||||
if call := exC.ExprCall(); call != nil {
|
||||
return call, nil
|
||||
}
|
||||
}
|
||||
return nil, Diagnostics{
|
||||
&Diagnostic{
|
||||
Severity: DiagError,
|
||||
Summary: "Invalid expression",
|
||||
Detail: "A static function call is required.",
|
||||
Subject: expr.StartRange().Ptr(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// StaticCall represents a function call that was extracted statically from
|
||||
// an expression using ExprCall.
|
||||
type StaticCall struct {
|
||||
Name string
|
||||
NameRange Range
|
||||
Arguments []Expression
|
||||
ArgsRange Range
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package hcl
|
||||
|
||||
// ExprMap tests if the given expression is a static map construct and,
|
||||
// if so, extracts the expressions that represent the map elements.
|
||||
// If the given expression is not a static map, error diagnostics are
|
||||
// returned.
|
||||
//
|
||||
// A particular Expression implementation can support this function by
|
||||
// offering a method called ExprMap that takes no arguments and returns
|
||||
// []KeyValuePair. This method should return nil if a static map cannot
|
||||
// be extracted. Alternatively, an implementation can support
|
||||
// UnwrapExpression to delegate handling of this function to a wrapped
|
||||
// Expression object.
|
||||
func ExprMap(expr Expression) ([]KeyValuePair, Diagnostics) {
|
||||
type exprMap interface {
|
||||
ExprMap() []KeyValuePair
|
||||
}
|
||||
|
||||
physExpr := UnwrapExpressionUntil(expr, func(expr Expression) bool {
|
||||
_, supported := expr.(exprMap)
|
||||
return supported
|
||||
})
|
||||
|
||||
if exM, supported := physExpr.(exprMap); supported {
|
||||
if pairs := exM.ExprMap(); pairs != nil {
|
||||
return pairs, nil
|
||||
}
|
||||
}
|
||||
return nil, Diagnostics{
|
||||
&Diagnostic{
|
||||
Severity: DiagError,
|
||||
Summary: "Invalid expression",
|
||||
Detail: "A static map expression is required.",
|
||||
Subject: expr.StartRange().Ptr(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// KeyValuePair represents a pair of expressions that serve as a single item
|
||||
// within a map or object definition construct.
|
||||
type KeyValuePair struct {
|
||||
Key Expression
|
||||
Value Expression
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2018 Martin Atkins
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -1,6 +1,6 @@
|
||||
// Package function builds on the functionality of cty by modeling functions
|
||||
// that operate on cty Values.
|
||||
//
|
||||
// Functions are, at their call, Go anonymous functions. However, this package
|
||||
// Functions are, at their core, Go anonymous functions. However, this package
|
||||
// wraps around them utility functions for parameter type checking, etc.
|
||||
package function
|
||||
|
||||
@ -0,0 +1,93 @@
|
||||
package stdlib
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
)
|
||||
|
||||
var CSVDecodeFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "str",
|
||||
Type: cty.String,
|
||||
},
|
||||
},
|
||||
Type: func(args []cty.Value) (cty.Type, error) {
|
||||
str := args[0]
|
||||
if !str.IsKnown() {
|
||||
return cty.DynamicPseudoType, nil
|
||||
}
|
||||
|
||||
r := strings.NewReader(str.AsString())
|
||||
cr := csv.NewReader(r)
|
||||
headers, err := cr.Read()
|
||||
if err == io.EOF {
|
||||
return cty.DynamicPseudoType, fmt.Errorf("missing header line")
|
||||
}
|
||||
if err != nil {
|
||||
return cty.DynamicPseudoType, err
|
||||
}
|
||||
|
||||
atys := make(map[string]cty.Type, len(headers))
|
||||
for _, name := range headers {
|
||||
if _, exists := atys[name]; exists {
|
||||
return cty.DynamicPseudoType, fmt.Errorf("duplicate column name %q", name)
|
||||
}
|
||||
atys[name] = cty.String
|
||||
}
|
||||
return cty.List(cty.Object(atys)), nil
|
||||
},
|
||||
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||||
ety := retType.ElementType()
|
||||
atys := ety.AttributeTypes()
|
||||
str := args[0]
|
||||
r := strings.NewReader(str.AsString())
|
||||
cr := csv.NewReader(r)
|
||||
cr.FieldsPerRecord = len(atys)
|
||||
|
||||
// Read the header row first, since that'll tell us which indices
|
||||
// map to which attribute names.
|
||||
headers, err := cr.Read()
|
||||
if err != nil {
|
||||
return cty.DynamicVal, err
|
||||
}
|
||||
|
||||
var rows []cty.Value
|
||||
for {
|
||||
cols, err := cr.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return cty.DynamicVal, err
|
||||
}
|
||||
|
||||
vals := make(map[string]cty.Value, len(cols))
|
||||
for i, str := range cols {
|
||||
name := headers[i]
|
||||
vals[name] = cty.StringVal(str)
|
||||
}
|
||||
rows = append(rows, cty.ObjectVal(vals))
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
return cty.ListValEmpty(ety), nil
|
||||
}
|
||||
return cty.ListVal(rows), nil
|
||||
},
|
||||
})
|
||||
|
||||
// CSVDecode parses the given CSV (RFC 4180) string and, if it is valid,
|
||||
// returns a list of objects representing the rows.
|
||||
//
|
||||
// The result is always a list of some object type. The first row of the
|
||||
// input is used to determine the object attributes, and subsequent rows
|
||||
// determine the values of those attributes.
|
||||
func CSVDecode(str cty.Value) (cty.Value, error) {
|
||||
return CSVDecodeFunc.Call([]cty.Value{str})
|
||||
}
|
||||
@ -0,0 +1,496 @@
|
||||
package stdlib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/apparentlymart/go-textseg/textseg"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
"github.com/zclconf/go-cty/cty/json"
|
||||
)
|
||||
|
||||
//go:generate ragel -Z format_fsm.rl
|
||||
//go:generate gofmt -w format_fsm.go
|
||||
|
||||
var FormatFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "format",
|
||||
Type: cty.String,
|
||||
},
|
||||
},
|
||||
VarParam: &function.Parameter{
|
||||
Name: "args",
|
||||
Type: cty.DynamicPseudoType,
|
||||
AllowNull: true,
|
||||
},
|
||||
Type: function.StaticReturnType(cty.String),
|
||||
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||||
for _, arg := range args[1:] {
|
||||
if !arg.IsWhollyKnown() {
|
||||
// We require all nested values to be known because the only
|
||||
// thing we can do for a collection/structural type is print
|
||||
// it as JSON and that requires it to be wholly known.
|
||||
return cty.UnknownVal(cty.String), nil
|
||||
}
|
||||
}
|
||||
str, err := formatFSM(args[0].AsString(), args[1:])
|
||||
return cty.StringVal(str), err
|
||||
},
|
||||
})
|
||||
|
||||
var FormatListFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "format",
|
||||
Type: cty.String,
|
||||
},
|
||||
},
|
||||
VarParam: &function.Parameter{
|
||||
Name: "args",
|
||||
Type: cty.DynamicPseudoType,
|
||||
AllowNull: true,
|
||||
AllowUnknown: true,
|
||||
},
|
||||
Type: function.StaticReturnType(cty.List(cty.String)),
|
||||
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||||
fmtVal := args[0]
|
||||
args = args[1:]
|
||||
|
||||
if len(args) == 0 {
|
||||
// With no arguments, this function is equivalent to Format, but
|
||||
// returning a single-element list result.
|
||||
result, err := Format(fmtVal, args...)
|
||||
return cty.ListVal([]cty.Value{result}), err
|
||||
}
|
||||
|
||||
fmtStr := fmtVal.AsString()
|
||||
|
||||
// Each of our arguments will be dealt with either as an iterator
|
||||
// or as a single value. Iterators are used for sequence-type values
|
||||
// (lists, sets, tuples) while everything else is treated as a
|
||||
// single value. The sequences we iterate over are required to be
|
||||
// all the same length.
|
||||
iterLen := -1
|
||||
lenChooser := -1
|
||||
iterators := make([]cty.ElementIterator, len(args))
|
||||
singleVals := make([]cty.Value, len(args))
|
||||
for i, arg := range args {
|
||||
argTy := arg.Type()
|
||||
switch {
|
||||
case (argTy.IsListType() || argTy.IsSetType() || argTy.IsTupleType()) && !arg.IsNull():
|
||||
thisLen := arg.LengthInt()
|
||||
if iterLen == -1 {
|
||||
iterLen = thisLen
|
||||
lenChooser = i
|
||||
} else {
|
||||
if thisLen != iterLen {
|
||||
return cty.NullVal(cty.List(cty.String)), function.NewArgErrorf(
|
||||
i+1,
|
||||
"argument %d has length %d, which is inconsistent with argument %d of length %d",
|
||||
i+1, thisLen,
|
||||
lenChooser+1, iterLen,
|
||||
)
|
||||
}
|
||||
}
|
||||
iterators[i] = arg.ElementIterator()
|
||||
default:
|
||||
singleVals[i] = arg
|
||||
}
|
||||
}
|
||||
|
||||
if iterLen == 0 {
|
||||
// If our sequences are all empty then our result must be empty.
|
||||
return cty.ListValEmpty(cty.String), nil
|
||||
}
|
||||
|
||||
if iterLen == -1 {
|
||||
// If we didn't encounter any iterables at all then we're going
|
||||
// to just do one iteration with items from singleVals.
|
||||
iterLen = 1
|
||||
}
|
||||
|
||||
ret := make([]cty.Value, 0, iterLen)
|
||||
fmtArgs := make([]cty.Value, len(iterators))
|
||||
Results:
|
||||
for iterIdx := 0; iterIdx < iterLen; iterIdx++ {
|
||||
|
||||
// Construct our arguments for a single format call
|
||||
for i := range fmtArgs {
|
||||
switch {
|
||||
case iterators[i] != nil:
|
||||
iterator := iterators[i]
|
||||
iterator.Next()
|
||||
_, val := iterator.Element()
|
||||
fmtArgs[i] = val
|
||||
default:
|
||||
fmtArgs[i] = singleVals[i]
|
||||
}
|
||||
|
||||
// If any of the arguments to this call would be unknown then
|
||||
// this particular result is unknown, but we'll keep going
|
||||
// to see if any other iterations can produce known values.
|
||||
if !fmtArgs[i].IsWhollyKnown() {
|
||||
// We require all nested values to be known because the only
|
||||
// thing we can do for a collection/structural type is print
|
||||
// it as JSON and that requires it to be wholly known.
|
||||
ret = append(ret, cty.UnknownVal(cty.String))
|
||||
continue Results
|
||||
}
|
||||
}
|
||||
|
||||
str, err := formatFSM(fmtStr, fmtArgs)
|
||||
if err != nil {
|
||||
return cty.NullVal(cty.List(cty.String)), fmt.Errorf(
|
||||
"error on format iteration %d: %s", iterIdx, err,
|
||||
)
|
||||
}
|
||||
|
||||
ret = append(ret, cty.StringVal(str))
|
||||
}
|
||||
|
||||
return cty.ListVal(ret), nil
|
||||
},
|
||||
})
|
||||
|
||||
// Format produces a string representation of zero or more values using a
|
||||
// format string similar to the "printf" function in C.
|
||||
//
|
||||
// It supports the following "verbs":
|
||||
//
|
||||
// %% Literal percent sign, consuming no value
|
||||
// %v A default formatting of the value based on type, as described below.
|
||||
// %#v JSON serialization of the value
|
||||
// %t Converts to boolean and then produces "true" or "false"
|
||||
// %b Converts to number, requires integer, produces binary representation
|
||||
// %d Converts to number, requires integer, produces decimal representation
|
||||
// %o Converts to number, requires integer, produces octal representation
|
||||
// %x Converts to number, requires integer, produces hexadecimal representation
|
||||
// with lowercase letters
|
||||
// %X Like %x but with uppercase letters
|
||||
// %e Converts to number, produces scientific notation like -1.234456e+78
|
||||
// %E Like %e but with an uppercase "E" representing the exponent
|
||||
// %f Converts to number, produces decimal representation with fractional
|
||||
// part but no exponent, like 123.456
|
||||
// %g %e for large exponents or %f otherwise
|
||||
// %G %E for large exponents or %f otherwise
|
||||
// %s Converts to string and produces the string's characters
|
||||
// %q Converts to string and produces JSON-quoted string representation,
|
||||
// like %v.
|
||||
//
|
||||
// The default format selections made by %v are:
|
||||
//
|
||||
// string %s
|
||||
// number %g
|
||||
// bool %t
|
||||
// other %#v
|
||||
//
|
||||
// Null values produce the literal keyword "null" for %v and %#v, and produce
|
||||
// an error otherwise.
|
||||
//
|
||||
// Width is specified by an optional decimal number immediately preceding the
|
||||
// verb letter. If absent, the width is whatever is necessary to represent the
|
||||
// value. Precision is specified after the (optional) width by a period
|
||||
// followed by a decimal number. If no period is present, a default precision
|
||||
// is used. A period with no following number is invalid.
|
||||
// For examples:
|
||||
//
|
||||
// %f default width, default precision
|
||||
// %9f width 9, default precision
|
||||
// %.2f default width, precision 2
|
||||
// %9.2f width 9, precision 2
|
||||
//
|
||||
// Width and precision are measured in unicode characters (grapheme clusters).
|
||||
//
|
||||
// For most values, width is the minimum number of characters to output,
|
||||
// padding the formatted form with spaces if necessary.
|
||||
//
|
||||
// For strings, precision limits the length of the input to be formatted (not
|
||||
// the size of the output), truncating if necessary.
|
||||
//
|
||||
// For numbers, width sets the minimum width of the field and precision sets
|
||||
// the number of places after the decimal, if appropriate, except that for
|
||||
// %g/%G precision sets the total number of significant digits.
|
||||
//
|
||||
// The following additional symbols can be used immediately after the percent
|
||||
// introducer as flags:
|
||||
//
|
||||
// (a space) leave a space where the sign would be if number is positive
|
||||
// + Include a sign for a number even if it is positive (numeric only)
|
||||
// - Pad with spaces on the left rather than the right
|
||||
// 0 Pad with zeros rather than spaces.
|
||||
//
|
||||
// Flag characters are ignored for verbs that do not support them.
|
||||
//
|
||||
// By default, % sequences consume successive arguments starting with the first.
|
||||
// Introducing a [n] sequence immediately before the verb letter, where n is a
|
||||
// decimal integer, explicitly chooses a particular value argument by its
|
||||
// one-based index. Subsequent calls without an explicit index will then
|
||||
// proceed with n+1, n+2, etc.
|
||||
//
|
||||
// An error is produced if the format string calls for an impossible conversion
|
||||
// or accesses more values than are given. An error is produced also for
|
||||
// an unsupported format verb.
|
||||
func Format(format cty.Value, vals ...cty.Value) (cty.Value, error) {
|
||||
args := make([]cty.Value, 0, len(vals)+1)
|
||||
args = append(args, format)
|
||||
args = append(args, vals...)
|
||||
return FormatFunc.Call(args)
|
||||
}
|
||||
|
||||
// FormatList applies the same formatting behavior as Format, but accepts
|
||||
// a mixture of list and non-list values as arguments. Any list arguments
|
||||
// passed must have the same length, which dictates the length of the
|
||||
// resulting list.
|
||||
//
|
||||
// Any non-list arguments are used repeatedly for each iteration over the
|
||||
// list arguments. The list arguments are iterated in order by key, so
|
||||
// corresponding items are formatted together.
|
||||
func FormatList(format cty.Value, vals ...cty.Value) (cty.Value, error) {
|
||||
args := make([]cty.Value, 0, len(vals)+1)
|
||||
args = append(args, format)
|
||||
args = append(args, vals...)
|
||||
return FormatListFunc.Call(args)
|
||||
}
|
||||
|
||||
type formatVerb struct {
|
||||
Raw string
|
||||
Offset int
|
||||
|
||||
ArgNum int
|
||||
Mode rune
|
||||
|
||||
Zero bool
|
||||
Sharp bool
|
||||
Plus bool
|
||||
Minus bool
|
||||
Space bool
|
||||
|
||||
HasPrec bool
|
||||
Prec int
|
||||
|
||||
HasWidth bool
|
||||
Width int
|
||||
}
|
||||
|
||||
// formatAppend is called by formatFSM (generated by format_fsm.rl) for each
|
||||
// formatting sequence that is encountered.
|
||||
func formatAppend(verb *formatVerb, buf *bytes.Buffer, args []cty.Value) error {
|
||||
argIdx := verb.ArgNum - 1
|
||||
if argIdx >= len(args) {
|
||||
return fmt.Errorf(
|
||||
"not enough arguments for %q at %d: need index %d but have %d total",
|
||||
verb.Raw, verb.Offset,
|
||||
verb.ArgNum, len(args),
|
||||
)
|
||||
}
|
||||
arg := args[argIdx]
|
||||
|
||||
if verb.Mode != 'v' && arg.IsNull() {
|
||||
return fmt.Errorf("unsupported value for %q at %d: null value cannot be formatted", verb.Raw, verb.Offset)
|
||||
}
|
||||
|
||||
// Normalize to make some things easier for downstream formatters
|
||||
if !verb.HasWidth {
|
||||
verb.Width = -1
|
||||
}
|
||||
if !verb.HasPrec {
|
||||
verb.Prec = -1
|
||||
}
|
||||
|
||||
// For our first pass we'll ensure the verb is supported and then fan
|
||||
// out to other functions based on what conversion is needed.
|
||||
switch verb.Mode {
|
||||
|
||||
case 'v':
|
||||
return formatAppendAsIs(verb, buf, arg)
|
||||
|
||||
case 't':
|
||||
return formatAppendBool(verb, buf, arg)
|
||||
|
||||
case 'b', 'd', 'o', 'x', 'X', 'e', 'E', 'f', 'g', 'G':
|
||||
return formatAppendNumber(verb, buf, arg)
|
||||
|
||||
case 's', 'q':
|
||||
return formatAppendString(verb, buf, arg)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported format verb %q in %q at offset %d", verb.Mode, verb.Raw, verb.Offset)
|
||||
}
|
||||
}
|
||||
|
||||
func formatAppendAsIs(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
|
||||
|
||||
if !verb.Sharp && !arg.IsNull() {
|
||||
// Unless the caller overrode it with the sharp flag, we'll try some
|
||||
// specialized formats before we fall back on JSON.
|
||||
switch arg.Type() {
|
||||
case cty.String:
|
||||
fmted := arg.AsString()
|
||||
fmted = formatPadWidth(verb, fmted)
|
||||
buf.WriteString(fmted)
|
||||
return nil
|
||||
case cty.Number:
|
||||
bf := arg.AsBigFloat()
|
||||
fmted := bf.Text('g', -1)
|
||||
fmted = formatPadWidth(verb, fmted)
|
||||
buf.WriteString(fmted)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
jb, err := json.Marshal(arg, arg.Type())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err)
|
||||
}
|
||||
fmted := formatPadWidth(verb, string(jb))
|
||||
buf.WriteString(fmted)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatAppendBool(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
|
||||
var err error
|
||||
arg, err = convert.Convert(arg, cty.Bool)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err)
|
||||
}
|
||||
|
||||
if arg.True() {
|
||||
buf.WriteString("true")
|
||||
} else {
|
||||
buf.WriteString("false")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatAppendNumber(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
|
||||
var err error
|
||||
arg, err = convert.Convert(arg, cty.Number)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err)
|
||||
}
|
||||
|
||||
switch verb.Mode {
|
||||
case 'b', 'd', 'o', 'x', 'X':
|
||||
return formatAppendInteger(verb, buf, arg)
|
||||
default:
|
||||
bf := arg.AsBigFloat()
|
||||
|
||||
// For floats our format syntax is a subset of Go's, so it's
|
||||
// safe for us to just lean on the existing Go implementation.
|
||||
fmtstr := formatStripIndexSegment(verb.Raw)
|
||||
fmted := fmt.Sprintf(fmtstr, bf)
|
||||
buf.WriteString(fmted)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func formatAppendInteger(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
|
||||
bf := arg.AsBigFloat()
|
||||
bi, acc := bf.Int(nil)
|
||||
if acc != big.Exact {
|
||||
return fmt.Errorf("unsupported value for %q at %d: an integer is required", verb.Raw, verb.Offset)
|
||||
}
|
||||
|
||||
// For integers our format syntax is a subset of Go's, so it's
|
||||
// safe for us to just lean on the existing Go implementation.
|
||||
fmtstr := formatStripIndexSegment(verb.Raw)
|
||||
fmted := fmt.Sprintf(fmtstr, bi)
|
||||
buf.WriteString(fmted)
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatAppendString(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
|
||||
var err error
|
||||
arg, err = convert.Convert(arg, cty.String)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err)
|
||||
}
|
||||
|
||||
// We _cannot_ directly use the Go fmt.Sprintf implementation for strings
|
||||
// because it measures widths and precisions in runes rather than grapheme
|
||||
// clusters.
|
||||
|
||||
str := arg.AsString()
|
||||
if verb.Prec > 0 {
|
||||
strB := []byte(str)
|
||||
pos := 0
|
||||
wanted := verb.Prec
|
||||
for i := 0; i < wanted; i++ {
|
||||
next := strB[pos:]
|
||||
if len(next) == 0 {
|
||||
// ran out of characters before we hit our max width
|
||||
break
|
||||
}
|
||||
d, _, _ := textseg.ScanGraphemeClusters(strB[pos:], true)
|
||||
pos += d
|
||||
}
|
||||
str = str[:pos]
|
||||
}
|
||||
|
||||
switch verb.Mode {
|
||||
case 's':
|
||||
fmted := formatPadWidth(verb, str)
|
||||
buf.WriteString(fmted)
|
||||
case 'q':
|
||||
jb, err := json.Marshal(cty.StringVal(str), cty.String)
|
||||
if err != nil {
|
||||
// Should never happen, since we know this is a known, non-null string
|
||||
panic(fmt.Errorf("failed to marshal %#v as JSON: %s", arg, err))
|
||||
}
|
||||
fmted := formatPadWidth(verb, string(jb))
|
||||
buf.WriteString(fmted)
|
||||
default:
|
||||
// Should never happen because formatAppend should've already validated
|
||||
panic(fmt.Errorf("invalid string formatting mode %q", verb.Mode))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatPadWidth(verb *formatVerb, fmted string) string {
|
||||
if verb.Width < 0 {
|
||||
return fmted
|
||||
}
|
||||
|
||||
// Safe to ignore errors because ScanGraphemeClusters cannot produce errors
|
||||
givenLen, _ := textseg.TokenCount([]byte(fmted), textseg.ScanGraphemeClusters)
|
||||
wantLen := verb.Width
|
||||
if givenLen >= wantLen {
|
||||
return fmted
|
||||
}
|
||||
|
||||
padLen := wantLen - givenLen
|
||||
padChar := " "
|
||||
if verb.Zero {
|
||||
padChar = "0"
|
||||
}
|
||||
pads := strings.Repeat(padChar, padLen)
|
||||
|
||||
if verb.Minus {
|
||||
return fmted + pads
|
||||
}
|
||||
return pads + fmted
|
||||
}
|
||||
|
||||
// formatStripIndexSegment strips out any [nnn] segment present in a verb
|
||||
// string so that we can pass it through to Go's fmt.Sprintf with a single
|
||||
// argument. This is used in cases where we're just leaning on Go's formatter
|
||||
// because it's a superset of ours.
|
||||
func formatStripIndexSegment(rawVerb string) string {
|
||||
// We assume the string has already been validated here, since we should
|
||||
// only be using this function with strings that were accepted by our
|
||||
// scanner in formatFSM.
|
||||
start := strings.Index(rawVerb, "[")
|
||||
end := strings.Index(rawVerb, "]")
|
||||
if start == -1 || end == -1 {
|
||||
return rawVerb
|
||||
}
|
||||
|
||||
return rawVerb[:start] + rawVerb[end+1:]
|
||||
}
|
||||
@ -0,0 +1,358 @@
|
||||
// line 1 "format_fsm.rl"
|
||||
// This file is generated from format_fsm.rl. DO NOT EDIT.
|
||||
|
||||
// line 5 "format_fsm.rl"
|
||||
|
||||
package stdlib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// line 20 "format_fsm.go"
|
||||
var _formatfsm_actions []byte = []byte{
|
||||
0, 1, 0, 1, 1, 1, 2, 1, 4,
|
||||
1, 5, 1, 6, 1, 7, 1, 8,
|
||||
1, 9, 1, 10, 1, 11, 1, 14,
|
||||
1, 16, 1, 17, 1, 18, 2, 3,
|
||||
4, 2, 12, 10, 2, 12, 16, 2,
|
||||
12, 18, 2, 13, 14, 2, 15, 10,
|
||||
2, 15, 18,
|
||||
}
|
||||
|
||||
var _formatfsm_key_offsets []byte = []byte{
|
||||
0, 0, 14, 27, 34, 36, 39, 43,
|
||||
51,
|
||||
}
|
||||
|
||||
var _formatfsm_trans_keys []byte = []byte{
|
||||
32, 35, 37, 43, 45, 46, 48, 91,
|
||||
49, 57, 65, 90, 97, 122, 32, 35,
|
||||
43, 45, 46, 48, 91, 49, 57, 65,
|
||||
90, 97, 122, 91, 48, 57, 65, 90,
|
||||
97, 122, 49, 57, 93, 48, 57, 65,
|
||||
90, 97, 122, 46, 91, 48, 57, 65,
|
||||
90, 97, 122, 37,
|
||||
}
|
||||
|
||||
var _formatfsm_single_lengths []byte = []byte{
|
||||
0, 8, 7, 1, 0, 1, 0, 2,
|
||||
1,
|
||||
}
|
||||
|
||||
var _formatfsm_range_lengths []byte = []byte{
|
||||
0, 3, 3, 3, 1, 1, 2, 3,
|
||||
0,
|
||||
}
|
||||
|
||||
var _formatfsm_index_offsets []byte = []byte{
|
||||
0, 0, 12, 23, 28, 30, 33, 36,
|
||||
42,
|
||||
}
|
||||
|
||||
var _formatfsm_indicies []byte = []byte{
|
||||
1, 2, 3, 4, 5, 6, 7, 10,
|
||||
8, 9, 9, 0, 1, 2, 4, 5,
|
||||
6, 7, 10, 8, 9, 9, 0, 13,
|
||||
11, 12, 12, 0, 14, 0, 15, 14,
|
||||
0, 9, 9, 0, 16, 19, 17, 18,
|
||||
18, 0, 20, 3,
|
||||
}
|
||||
|
||||
var _formatfsm_trans_targs []byte = []byte{
|
||||
0, 2, 2, 8, 2, 2, 3, 2,
|
||||
7, 8, 4, 3, 8, 4, 5, 6,
|
||||
3, 7, 8, 4, 1,
|
||||
}
|
||||
|
||||
var _formatfsm_trans_actions []byte = []byte{
|
||||
7, 17, 9, 3, 15, 13, 25, 11,
|
||||
43, 29, 19, 27, 49, 46, 21, 0,
|
||||
37, 23, 40, 34, 1,
|
||||
}
|
||||
|
||||
var _formatfsm_eof_actions []byte = []byte{
|
||||
0, 31, 31, 31, 31, 31, 31, 31,
|
||||
5,
|
||||
}
|
||||
|
||||
const formatfsm_start int = 8
|
||||
const formatfsm_first_final int = 8
|
||||
const formatfsm_error int = 0
|
||||
|
||||
const formatfsm_en_main int = 8
|
||||
|
||||
// line 19 "format_fsm.rl"
|
||||
|
||||
func formatFSM(format string, a []cty.Value) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
data := format
|
||||
nextArg := 1 // arg numbers are 1-based
|
||||
var verb formatVerb
|
||||
|
||||
// line 153 "format_fsm.rl"
|
||||
|
||||
// Ragel state
|
||||
p := 0 // "Pointer" into data
|
||||
pe := len(data) // End-of-data "pointer"
|
||||
cs := 0 // current state (will be initialized by ragel-generated code)
|
||||
ts := 0
|
||||
te := 0
|
||||
eof := pe
|
||||
|
||||
// Keep Go compiler happy even if generated code doesn't use these
|
||||
_ = ts
|
||||
_ = te
|
||||
_ = eof
|
||||
|
||||
// line 121 "format_fsm.go"
|
||||
{
|
||||
cs = formatfsm_start
|
||||
}
|
||||
|
||||
// line 126 "format_fsm.go"
|
||||
{
|
||||
var _klen int
|
||||
var _trans int
|
||||
var _acts int
|
||||
var _nacts uint
|
||||
var _keys int
|
||||
if p == pe {
|
||||
goto _test_eof
|
||||
}
|
||||
if cs == 0 {
|
||||
goto _out
|
||||
}
|
||||
_resume:
|
||||
_keys = int(_formatfsm_key_offsets[cs])
|
||||
_trans = int(_formatfsm_index_offsets[cs])
|
||||
|
||||
_klen = int(_formatfsm_single_lengths[cs])
|
||||
if _klen > 0 {
|
||||
_lower := int(_keys)
|
||||
var _mid int
|
||||
_upper := int(_keys + _klen - 1)
|
||||
for {
|
||||
if _upper < _lower {
|
||||
break
|
||||
}
|
||||
|
||||
_mid = _lower + ((_upper - _lower) >> 1)
|
||||
switch {
|
||||
case data[p] < _formatfsm_trans_keys[_mid]:
|
||||
_upper = _mid - 1
|
||||
case data[p] > _formatfsm_trans_keys[_mid]:
|
||||
_lower = _mid + 1
|
||||
default:
|
||||
_trans += int(_mid - int(_keys))
|
||||
goto _match
|
||||
}
|
||||
}
|
||||
_keys += _klen
|
||||
_trans += _klen
|
||||
}
|
||||
|
||||
_klen = int(_formatfsm_range_lengths[cs])
|
||||
if _klen > 0 {
|
||||
_lower := int(_keys)
|
||||
var _mid int
|
||||
_upper := int(_keys + (_klen << 1) - 2)
|
||||
for {
|
||||
if _upper < _lower {
|
||||
break
|
||||
}
|
||||
|
||||
_mid = _lower + (((_upper - _lower) >> 1) & ^1)
|
||||
switch {
|
||||
case data[p] < _formatfsm_trans_keys[_mid]:
|
||||
_upper = _mid - 2
|
||||
case data[p] > _formatfsm_trans_keys[_mid+1]:
|
||||
_lower = _mid + 2
|
||||
default:
|
||||
_trans += int((_mid - int(_keys)) >> 1)
|
||||
goto _match
|
||||
}
|
||||
}
|
||||
_trans += _klen
|
||||
}
|
||||
|
||||
_match:
|
||||
_trans = int(_formatfsm_indicies[_trans])
|
||||
cs = int(_formatfsm_trans_targs[_trans])
|
||||
|
||||
if _formatfsm_trans_actions[_trans] == 0 {
|
||||
goto _again
|
||||
}
|
||||
|
||||
_acts = int(_formatfsm_trans_actions[_trans])
|
||||
_nacts = uint(_formatfsm_actions[_acts])
|
||||
_acts++
|
||||
for ; _nacts > 0; _nacts-- {
|
||||
_acts++
|
||||
switch _formatfsm_actions[_acts-1] {
|
||||
case 0:
|
||||
// line 29 "format_fsm.rl"
|
||||
|
||||
verb = formatVerb{
|
||||
ArgNum: nextArg,
|
||||
Prec: -1,
|
||||
Width: -1,
|
||||
}
|
||||
ts = p
|
||||
|
||||
case 1:
|
||||
// line 38 "format_fsm.rl"
|
||||
|
||||
buf.WriteByte(data[p])
|
||||
|
||||
case 4:
|
||||
// line 49 "format_fsm.rl"
|
||||
|
||||
// We'll try to slurp a whole UTF-8 sequence here, to give the user
|
||||
// better feedback.
|
||||
r, _ := utf8.DecodeRuneInString(data[p:])
|
||||
return buf.String(), fmt.Errorf("unrecognized format character %q at offset %d", r, p)
|
||||
|
||||
case 5:
|
||||
// line 56 "format_fsm.rl"
|
||||
|
||||
verb.Sharp = true
|
||||
|
||||
case 6:
|
||||
// line 59 "format_fsm.rl"
|
||||
|
||||
verb.Zero = true
|
||||
|
||||
case 7:
|
||||
// line 62 "format_fsm.rl"
|
||||
|
||||
verb.Minus = true
|
||||
|
||||
case 8:
|
||||
// line 65 "format_fsm.rl"
|
||||
|
||||
verb.Plus = true
|
||||
|
||||
case 9:
|
||||
// line 68 "format_fsm.rl"
|
||||
|
||||
verb.Space = true
|
||||
|
||||
case 10:
|
||||
// line 72 "format_fsm.rl"
|
||||
|
||||
verb.ArgNum = 0
|
||||
|
||||
case 11:
|
||||
// line 75 "format_fsm.rl"
|
||||
|
||||
verb.ArgNum = (10 * verb.ArgNum) + (int(data[p]) - '0')
|
||||
|
||||
case 12:
|
||||
// line 79 "format_fsm.rl"
|
||||
|
||||
verb.HasWidth = true
|
||||
|
||||
case 13:
|
||||
// line 82 "format_fsm.rl"
|
||||
|
||||
verb.Width = 0
|
||||
|
||||
case 14:
|
||||
// line 85 "format_fsm.rl"
|
||||
|
||||
verb.Width = (10 * verb.Width) + (int(data[p]) - '0')
|
||||
|
||||
case 15:
|
||||
// line 89 "format_fsm.rl"
|
||||
|
||||
verb.HasPrec = true
|
||||
|
||||
case 16:
|
||||
// line 92 "format_fsm.rl"
|
||||
|
||||
verb.Prec = 0
|
||||
|
||||
case 17:
|
||||
// line 95 "format_fsm.rl"
|
||||
|
||||
verb.Prec = (10 * verb.Prec) + (int(data[p]) - '0')
|
||||
|
||||
case 18:
|
||||
// line 99 "format_fsm.rl"
|
||||
|
||||
verb.Mode = rune(data[p])
|
||||
te = p + 1
|
||||
verb.Raw = data[ts:te]
|
||||
verb.Offset = ts
|
||||
|
||||
err := formatAppend(&verb, &buf, a)
|
||||
if err != nil {
|
||||
return buf.String(), err
|
||||
}
|
||||
nextArg = verb.ArgNum + 1
|
||||
|
||||
// line 324 "format_fsm.go"
|
||||
}
|
||||
}
|
||||
|
||||
_again:
|
||||
if cs == 0 {
|
||||
goto _out
|
||||
}
|
||||
p++
|
||||
if p != pe {
|
||||
goto _resume
|
||||
}
|
||||
_test_eof:
|
||||
{
|
||||
}
|
||||
if p == eof {
|
||||
__acts := _formatfsm_eof_actions[cs]
|
||||
__nacts := uint(_formatfsm_actions[__acts])
|
||||
__acts++
|
||||
for ; __nacts > 0; __nacts-- {
|
||||
__acts++
|
||||
switch _formatfsm_actions[__acts-1] {
|
||||
case 2:
|
||||
// line 42 "format_fsm.rl"
|
||||
|
||||
case 3:
|
||||
// line 45 "format_fsm.rl"
|
||||
|
||||
return buf.String(), fmt.Errorf("invalid format string starting at offset %d", p)
|
||||
|
||||
case 4:
|
||||
// line 49 "format_fsm.rl"
|
||||
|
||||
// We'll try to slurp a whole UTF-8 sequence here, to give the user
|
||||
// better feedback.
|
||||
r, _ := utf8.DecodeRuneInString(data[p:])
|
||||
return buf.String(), fmt.Errorf("unrecognized format character %q at offset %d", r, p)
|
||||
|
||||
// line 363 "format_fsm.go"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_out:
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// line 171 "format_fsm.rl"
|
||||
|
||||
// If we fall out here without being in a final state then we've
|
||||
// encountered something that the scanner can't match, which should
|
||||
// be impossible (the scanner matches all bytes _somehow_) but we'll
|
||||
// flag it anyway rather than just losing data from the end.
|
||||
if cs < formatfsm_first_final {
|
||||
return buf.String(), fmt.Errorf("extraneous characters beginning at offset %i", p)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
@ -0,0 +1,182 @@
|
||||
// This file is generated from format_fsm.rl. DO NOT EDIT.
|
||||
%%{
|
||||
# (except you are actually in scan_tokens.rl here, so edit away!)
|
||||
machine formatfsm;
|
||||
}%%
|
||||
|
||||
package stdlib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
%%{
|
||||
write data;
|
||||
}%%
|
||||
|
||||
func formatFSM(format string, a []cty.Value) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
data := format
|
||||
nextArg := 1 // arg numbers are 1-based
|
||||
var verb formatVerb
|
||||
|
||||
%%{
|
||||
|
||||
action begin {
|
||||
verb = formatVerb{
|
||||
ArgNum: nextArg,
|
||||
Prec: -1,
|
||||
Width: -1,
|
||||
}
|
||||
ts = p
|
||||
}
|
||||
|
||||
action emit {
|
||||
buf.WriteByte(fc);
|
||||
}
|
||||
|
||||
action finish_ok {
|
||||
}
|
||||
|
||||
action finish_err {
|
||||
return buf.String(), fmt.Errorf("invalid format string starting at offset %d", p)
|
||||
}
|
||||
|
||||
action err_char {
|
||||
// We'll try to slurp a whole UTF-8 sequence here, to give the user
|
||||
// better feedback.
|
||||
r, _ := utf8.DecodeRuneInString(data[p:])
|
||||
return buf.String(), fmt.Errorf("unrecognized format character %q at offset %d", r, p)
|
||||
}
|
||||
|
||||
action flag_sharp {
|
||||
verb.Sharp = true
|
||||
}
|
||||
action flag_zero {
|
||||
verb.Zero = true
|
||||
}
|
||||
action flag_minus {
|
||||
verb.Minus = true
|
||||
}
|
||||
action flag_plus {
|
||||
verb.Plus = true
|
||||
}
|
||||
action flag_space {
|
||||
verb.Space = true
|
||||
}
|
||||
|
||||
action argidx_reset {
|
||||
verb.ArgNum = 0
|
||||
}
|
||||
action argidx_num {
|
||||
verb.ArgNum = (10 * verb.ArgNum) + (int(fc) - '0')
|
||||
}
|
||||
|
||||
action has_width {
|
||||
verb.HasWidth = true
|
||||
}
|
||||
action width_reset {
|
||||
verb.Width = 0
|
||||
}
|
||||
action width_num {
|
||||
verb.Width = (10 * verb.Width) + (int(fc) - '0')
|
||||
}
|
||||
|
||||
action has_prec {
|
||||
verb.HasPrec = true
|
||||
}
|
||||
action prec_reset {
|
||||
verb.Prec = 0
|
||||
}
|
||||
action prec_num {
|
||||
verb.Prec = (10 * verb.Prec) + (int(fc) - '0')
|
||||
}
|
||||
|
||||
action mode {
|
||||
verb.Mode = rune(fc)
|
||||
te = p+1
|
||||
verb.Raw = data[ts:te]
|
||||
verb.Offset = ts
|
||||
|
||||
err := formatAppend(&verb, &buf, a)
|
||||
if err != nil {
|
||||
return buf.String(), err
|
||||
}
|
||||
nextArg = verb.ArgNum + 1
|
||||
}
|
||||
|
||||
# a number that isn't zero and doesn't have a leading zero
|
||||
num = [1-9] [0-9]*;
|
||||
|
||||
flags = (
|
||||
'0' @flag_zero |
|
||||
'#' @flag_sharp |
|
||||
'-' @flag_minus |
|
||||
'+' @flag_plus |
|
||||
' ' @flag_space
|
||||
)*;
|
||||
|
||||
argidx = ((
|
||||
'[' (num $argidx_num) ']'
|
||||
) >argidx_reset)?;
|
||||
|
||||
width = (
|
||||
( num $width_num ) >width_reset %has_width
|
||||
)?;
|
||||
|
||||
precision = (
|
||||
('.' ( digit* $prec_num )) >prec_reset %has_prec
|
||||
)?;
|
||||
|
||||
# We accept any letter here, but will be more picky in formatAppend
|
||||
mode = ('a'..'z' | 'A'..'Z') @mode;
|
||||
|
||||
fmt_verb = (
|
||||
'%' @begin
|
||||
flags
|
||||
width
|
||||
precision
|
||||
argidx
|
||||
mode
|
||||
);
|
||||
|
||||
main := (
|
||||
[^%] @emit |
|
||||
'%%' @emit |
|
||||
fmt_verb
|
||||
)* @/finish_err %/finish_ok $!err_char;
|
||||
|
||||
}%%
|
||||
|
||||
// Ragel state
|
||||
p := 0 // "Pointer" into data
|
||||
pe := len(data) // End-of-data "pointer"
|
||||
cs := 0 // current state (will be initialized by ragel-generated code)
|
||||
ts := 0
|
||||
te := 0
|
||||
eof := pe
|
||||
|
||||
// Keep Go compiler happy even if generated code doesn't use these
|
||||
_ = ts
|
||||
_ = te
|
||||
_ = eof
|
||||
|
||||
%%{
|
||||
write init;
|
||||
write exec;
|
||||
}%%
|
||||
|
||||
// If we fall out here without being in a final state then we've
|
||||
// encountered something that the scanner can't match, which should
|
||||
// be impossible (the scanner matches all bytes _somehow_) but we'll
|
||||
// flag it anyway rather than just losing data from the end.
|
||||
if cs < formatfsm_first_final {
|
||||
return buf.String(), fmt.Errorf("extraneous characters beginning at offset %i", p)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
@ -0,0 +1,195 @@
|
||||
package stdlib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
)
|
||||
|
||||
var SetHasElementFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "set",
|
||||
Type: cty.Set(cty.DynamicPseudoType),
|
||||
AllowDynamicType: true,
|
||||
},
|
||||
{
|
||||
Name: "elem",
|
||||
Type: cty.DynamicPseudoType,
|
||||
AllowDynamicType: true,
|
||||
},
|
||||
},
|
||||
Type: function.StaticReturnType(cty.Bool),
|
||||
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
|
||||
return args[0].HasElement(args[1]), nil
|
||||
},
|
||||
})
|
||||
|
||||
var SetUnionFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "first_set",
|
||||
Type: cty.Set(cty.DynamicPseudoType),
|
||||
AllowDynamicType: true,
|
||||
},
|
||||
},
|
||||
VarParam: &function.Parameter{
|
||||
Name: "other_sets",
|
||||
Type: cty.Set(cty.DynamicPseudoType),
|
||||
AllowDynamicType: true,
|
||||
},
|
||||
Type: setOperationReturnType,
|
||||
Impl: setOperationImpl(func(s1, s2 cty.ValueSet) cty.ValueSet {
|
||||
return s1.Union(s2)
|
||||
}),
|
||||
})
|
||||
|
||||
var SetIntersectionFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "first_set",
|
||||
Type: cty.Set(cty.DynamicPseudoType),
|
||||
AllowDynamicType: true,
|
||||
},
|
||||
},
|
||||
VarParam: &function.Parameter{
|
||||
Name: "other_sets",
|
||||
Type: cty.Set(cty.DynamicPseudoType),
|
||||
AllowDynamicType: true,
|
||||
},
|
||||
Type: setOperationReturnType,
|
||||
Impl: setOperationImpl(func(s1, s2 cty.ValueSet) cty.ValueSet {
|
||||
return s1.Intersection(s2)
|
||||
}),
|
||||
})
|
||||
|
||||
var SetSubtractFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "a",
|
||||
Type: cty.Set(cty.DynamicPseudoType),
|
||||
AllowDynamicType: true,
|
||||
},
|
||||
{
|
||||
Name: "b",
|
||||
Type: cty.Set(cty.DynamicPseudoType),
|
||||
AllowDynamicType: true,
|
||||
},
|
||||
},
|
||||
Type: setOperationReturnType,
|
||||
Impl: setOperationImpl(func(s1, s2 cty.ValueSet) cty.ValueSet {
|
||||
return s1.Subtract(s2)
|
||||
}),
|
||||
})
|
||||
|
||||
var SetSymmetricDifferenceFunc = function.New(&function.Spec{
|
||||
Params: []function.Parameter{
|
||||
{
|
||||
Name: "first_set",
|
||||
Type: cty.Set(cty.DynamicPseudoType),
|
||||
AllowDynamicType: true,
|
||||
},
|
||||
},
|
||||
VarParam: &function.Parameter{
|
||||
Name: "other_sets",
|
||||
Type: cty.Set(cty.DynamicPseudoType),
|
||||
AllowDynamicType: true,
|
||||
},
|
||||
Type: setOperationReturnType,
|
||||
Impl: setOperationImpl(func(s1, s2 cty.ValueSet) cty.ValueSet {
|
||||
return s1.Subtract(s2)
|
||||
}),
|
||||
})
|
||||
|
||||
// SetHasElement determines whether the given set contains the given value as an
|
||||
// element.
|
||||
func SetHasElement(set cty.Value, elem cty.Value) (cty.Value, error) {
|
||||
return SetHasElementFunc.Call([]cty.Value{set, elem})
|
||||
}
|
||||
|
||||
// SetUnion returns a new set containing all of the elements from the given
|
||||
// sets, which must have element types that can all be converted to some
|
||||
// common type using the standard type unification rules. If conversion
|
||||
// is not possible, an error is returned.
|
||||
//
|
||||
// The union operation is performed after type conversion, which may result
|
||||
// in some previously-distinct values being conflated.
|
||||
//
|
||||
// At least one set must be provided.
|
||||
func SetUnion(sets ...cty.Value) (cty.Value, error) {
|
||||
return SetUnionFunc.Call(sets)
|
||||
}
|
||||
|
||||
// Intersection returns a new set containing the elements that exist
|
||||
// in all of the given sets, which must have element types that can all be
|
||||
// converted to some common type using the standard type unification rules.
|
||||
// If conversion is not possible, an error is returned.
|
||||
//
|
||||
// The intersection operation is performed after type conversion, which may
|
||||
// result in some previously-distinct values being conflated.
|
||||
//
|
||||
// At least one set must be provided.
|
||||
func SetIntersection(sets ...cty.Value) (cty.Value, error) {
|
||||
return SetIntersectionFunc.Call(sets)
|
||||
}
|
||||
|
||||
// SetSubtract returns a new set containing the elements from the
|
||||
// first set that are not present in the second set. The sets must have
|
||||
// element types that can both be converted to some common type using the
|
||||
// standard type unification rules. If conversion is not possible, an error
|
||||
// is returned.
|
||||
//
|
||||
// The subtract operation is performed after type conversion, which may
|
||||
// result in some previously-distinct values being conflated.
|
||||
func SetSubtract(a, b cty.Value) (cty.Value, error) {
|
||||
return SetSubtractFunc.Call([]cty.Value{a, b})
|
||||
}
|
||||
|
||||
// SetSymmetricDifference returns a new set containing elements that appear
|
||||
// in any of the given sets but not multiple. The sets must have
|
||||
// element types that can all be converted to some common type using the
|
||||
// standard type unification rules. If conversion is not possible, an error
|
||||
// is returned.
|
||||
//
|
||||
// The difference operation is performed after type conversion, which may
|
||||
// result in some previously-distinct values being conflated.
|
||||
func SetSymmetricDifference(sets ...cty.Value) (cty.Value, error) {
|
||||
return SetSymmetricDifferenceFunc.Call(sets)
|
||||
}
|
||||
|
||||
func setOperationReturnType(args []cty.Value) (ret cty.Type, err error) {
|
||||
var etys []cty.Type
|
||||
for _, arg := range args {
|
||||
etys = append(etys, arg.Type().ElementType())
|
||||
}
|
||||
newEty, _ := convert.UnifyUnsafe(etys)
|
||||
if newEty == cty.NilType {
|
||||
return cty.NilType, fmt.Errorf("given sets must all have compatible element types")
|
||||
}
|
||||
return cty.Set(newEty), nil
|
||||
}
|
||||
|
||||
func setOperationImpl(f func(s1, s2 cty.ValueSet) cty.ValueSet) function.ImplFunc {
|
||||
return func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
|
||||
first := args[0]
|
||||
first, err = convert.Convert(first, retType)
|
||||
if err != nil {
|
||||
return cty.NilVal, function.NewArgError(0, err)
|
||||
}
|
||||
|
||||
set := first.AsValueSet()
|
||||
for i, arg := range args[1:] {
|
||||
arg, err := convert.Convert(arg, retType)
|
||||
if err != nil {
|
||||
return cty.NilVal, function.NewArgError(i+1, err)
|
||||
}
|
||||
|
||||
argSet := arg.AsValueSet()
|
||||
set = f(set, argSet)
|
||||
}
|
||||
return cty.SetValFromValueSet(set), nil
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package function
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// Unpredictable wraps a given function such that it retains the same arguments
|
||||
// and type checking behavior but will return an unknown value when called.
|
||||
//
|
||||
// It is recommended that most functions be "pure", which is to say that they
|
||||
// will always produce the same value given particular input. However,
|
||||
// sometimes it is necessary to offer functions whose behavior depends on
|
||||
// some external state, such as reading a file or determining the current time.
|
||||
// In such cases, an unpredictable wrapper might be used to stand in for
|
||||
// the function during some sort of prior "checking" phase in order to delay
|
||||
// the actual effect until later.
|
||||
//
|
||||
// While Unpredictable can support a function that isn't pure in its
|
||||
// implementation, it still expects a function to be pure in its type checking
|
||||
// behavior, except for the special case of returning cty.DynamicPseudoType
|
||||
// if it is not yet able to predict its return value based on current argument
|
||||
// information.
|
||||
func Unpredictable(f Function) Function {
|
||||
newSpec := *f.spec // shallow copy
|
||||
newSpec.Impl = unpredictableImpl
|
||||
return New(&newSpec)
|
||||
}
|
||||
|
||||
func unpredictableImpl(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||||
return cty.UnknownVal(retType), nil
|
||||
}
|
||||
@ -0,0 +1,126 @@
|
||||
package cty
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zclconf/go-cty/cty/set"
|
||||
)
|
||||
|
||||
// ValueSet is to cty.Set what []cty.Value is to cty.List and
|
||||
// map[string]cty.Value is to cty.Map. It's provided to allow callers a
|
||||
// convenient interface for manipulating sets before wrapping them in cty.Set
|
||||
// values using cty.SetValFromValueSet.
|
||||
//
|
||||
// Unlike value slices and value maps, ValueSet instances have a single
|
||||
// homogenous element type because that is a requirement of the underlying
|
||||
// set implementation, which uses the element type to select a suitable
|
||||
// hashing function.
|
||||
//
|
||||
// Set mutations are not concurrency-safe.
|
||||
type ValueSet struct {
|
||||
// ValueSet is just a thin wrapper around a set.Set with our value-oriented
|
||||
// "rules" applied. We do this so that the caller can work in terms of
|
||||
// cty.Value objects even though the set internals use the raw values.
|
||||
s set.Set
|
||||
}
|
||||
|
||||
// NewValueSet creates and returns a new ValueSet with the given element type.
|
||||
func NewValueSet(ety Type) ValueSet {
|
||||
return newValueSet(set.NewSet(setRules{Type: ety}))
|
||||
}
|
||||
|
||||
func newValueSet(s set.Set) ValueSet {
|
||||
return ValueSet{
|
||||
s: s,
|
||||
}
|
||||
}
|
||||
|
||||
// ElementType returns the element type for the receiving ValueSet.
|
||||
func (s ValueSet) ElementType() Type {
|
||||
return s.s.Rules().(setRules).Type
|
||||
}
|
||||
|
||||
// Add inserts the given value into the receiving set.
|
||||
func (s ValueSet) Add(v Value) {
|
||||
s.requireElementType(v)
|
||||
s.s.Add(v.v)
|
||||
}
|
||||
|
||||
// Remove deletes the given value from the receiving set, if indeed it was
|
||||
// there in the first place. If the value is not present, this is a no-op.
|
||||
func (s ValueSet) Remove(v Value) {
|
||||
s.requireElementType(v)
|
||||
s.s.Remove(v.v)
|
||||
}
|
||||
|
||||
// Has returns true if the given value is in the receiving set, or false if
|
||||
// it is not.
|
||||
func (s ValueSet) Has(v Value) bool {
|
||||
s.requireElementType(v)
|
||||
return s.s.Has(v.v)
|
||||
}
|
||||
|
||||
// Copy performs a shallow copy of the receiving set, returning a new set
|
||||
// with the same rules and elements.
|
||||
func (s ValueSet) Copy() ValueSet {
|
||||
return newValueSet(s.s.Copy())
|
||||
}
|
||||
|
||||
// Length returns the number of values in the set.
|
||||
func (s ValueSet) Length() int {
|
||||
return s.s.Length()
|
||||
}
|
||||
|
||||
// Values returns a slice of all of the values in the set in no particular
|
||||
// order.
|
||||
func (s ValueSet) Values() []Value {
|
||||
l := s.s.Length()
|
||||
if l == 0 {
|
||||
return nil
|
||||
}
|
||||
ret := make([]Value, 0, l)
|
||||
ety := s.ElementType()
|
||||
for it := s.s.Iterator(); it.Next(); {
|
||||
ret = append(ret, Value{
|
||||
ty: ety,
|
||||
v: it.Value(),
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Union returns a new set that contains all of the members of both the
|
||||
// receiving set and the given set. Both sets must have the same element type,
|
||||
// or else this function will panic.
|
||||
func (s ValueSet) Union(other ValueSet) ValueSet {
|
||||
return newValueSet(s.s.Union(other.s))
|
||||
}
|
||||
|
||||
// Intersection returns a new set that contains the values that both the
|
||||
// receiver and given sets have in common. Both sets must have the same element
|
||||
// type, or else this function will panic.
|
||||
func (s ValueSet) Intersection(other ValueSet) ValueSet {
|
||||
return newValueSet(s.s.Intersection(other.s))
|
||||
}
|
||||
|
||||
// Subtract returns a new set that contains all of the values from the receiver
|
||||
// that are not also in the given set. Both sets must have the same element
|
||||
// type, or else this function will panic.
|
||||
func (s ValueSet) Subtract(other ValueSet) ValueSet {
|
||||
return newValueSet(s.s.Subtract(other.s))
|
||||
}
|
||||
|
||||
// SymmetricDifference returns a new set that contains all of the values from
|
||||
// both the receiver and given sets, except those that both sets have in
|
||||
// common. Both sets must have the same element type, or else this function
|
||||
// will panic.
|
||||
func (s ValueSet) SymmetricDifference(other ValueSet) ValueSet {
|
||||
return newValueSet(s.s.SymmetricDifference(other.s))
|
||||
}
|
||||
|
||||
// requireElementType panics if the given value is not of the set's element type.
|
||||
func (s ValueSet) requireElementType(v Value) {
|
||||
if !v.Type().Equals(s.ElementType()) {
|
||||
panic(fmt.Errorf("attempt to use %#v value with set of %#v", v.Type(), s.ElementType()))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,182 @@
|
||||
package cty
|
||||
|
||||
// Walk visits all of the values in a possibly-complex structure, calling
|
||||
// a given function for each value.
|
||||
//
|
||||
// For example, given a list of strings the callback would first be called
|
||||
// with the whole list and then called once for each element of the list.
|
||||
//
|
||||
// The callback function may prevent recursive visits to child values by
|
||||
// returning false. The callback function my halt the walk altogether by
|
||||
// returning a non-nil error. If the returned error is about the element
|
||||
// currently being visited, it is recommended to use the provided path
|
||||
// value to produce a PathError describing that context.
|
||||
//
|
||||
// The path passed to the given function may not be used after that function
|
||||
// returns, since its backing array is re-used for other calls.
|
||||
func Walk(val Value, cb func(Path, Value) (bool, error)) error {
|
||||
var path Path
|
||||
return walk(path, val, cb)
|
||||
}
|
||||
|
||||
func walk(path Path, val Value, cb func(Path, Value) (bool, error)) error {
|
||||
deeper, err := cb(path, val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !deeper {
|
||||
return nil
|
||||
}
|
||||
|
||||
if val.IsNull() || !val.IsKnown() {
|
||||
// Can't recurse into null or unknown values, regardless of type
|
||||
return nil
|
||||
}
|
||||
|
||||
ty := val.Type()
|
||||
switch {
|
||||
case ty.IsObjectType():
|
||||
for it := val.ElementIterator(); it.Next(); {
|
||||
nameVal, av := it.Element()
|
||||
path := append(path, GetAttrStep{
|
||||
Name: nameVal.AsString(),
|
||||
})
|
||||
err := walk(path, av, cb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case val.CanIterateElements():
|
||||
for it := val.ElementIterator(); it.Next(); {
|
||||
kv, ev := it.Element()
|
||||
path := append(path, IndexStep{
|
||||
Key: kv,
|
||||
})
|
||||
err := walk(path, ev, cb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Transform visits all of the values in a possibly-complex structure,
|
||||
// calling a given function for each value which has an opportunity to
|
||||
// replace that value.
|
||||
//
|
||||
// Unlike Walk, Transform visits child nodes first, so for a list of strings
|
||||
// it would first visit the strings and then the _new_ list constructed
|
||||
// from the transformed values of the list items.
|
||||
//
|
||||
// This is useful for creating the effect of being able to make deep mutations
|
||||
// to a value even though values are immutable. However, it's the responsibility
|
||||
// of the given function to preserve expected invariants, such as homogenity of
|
||||
// element types in collections; this function can panic if such invariants
|
||||
// are violated, just as if new values were constructed directly using the
|
||||
// value constructor functions. An easy way to preserve invariants is to
|
||||
// ensure that the transform function never changes the value type.
|
||||
//
|
||||
// The callback function my halt the walk altogether by
|
||||
// returning a non-nil error. If the returned error is about the element
|
||||
// currently being visited, it is recommended to use the provided path
|
||||
// value to produce a PathError describing that context.
|
||||
//
|
||||
// The path passed to the given function may not be used after that function
|
||||
// returns, since its backing array is re-used for other calls.
|
||||
func Transform(val Value, cb func(Path, Value) (Value, error)) (Value, error) {
|
||||
var path Path
|
||||
return transform(path, val, cb)
|
||||
}
|
||||
|
||||
func transform(path Path, val Value, cb func(Path, Value) (Value, error)) (Value, error) {
|
||||
ty := val.Type()
|
||||
var newVal Value
|
||||
|
||||
switch {
|
||||
|
||||
case val.IsNull() || !val.IsKnown():
|
||||
// Can't recurse into null or unknown values, regardless of type
|
||||
newVal = val
|
||||
|
||||
case ty.IsListType() || ty.IsSetType() || ty.IsTupleType():
|
||||
l := val.LengthInt()
|
||||
switch l {
|
||||
case 0:
|
||||
// No deep transform for an empty sequence
|
||||
newVal = val
|
||||
default:
|
||||
elems := make([]Value, 0, l)
|
||||
for it := val.ElementIterator(); it.Next(); {
|
||||
kv, ev := it.Element()
|
||||
path := append(path, IndexStep{
|
||||
Key: kv,
|
||||
})
|
||||
newEv, err := transform(path, ev, cb)
|
||||
if err != nil {
|
||||
return DynamicVal, err
|
||||
}
|
||||
elems = append(elems, newEv)
|
||||
}
|
||||
switch {
|
||||
case ty.IsListType():
|
||||
newVal = ListVal(elems)
|
||||
case ty.IsSetType():
|
||||
newVal = SetVal(elems)
|
||||
case ty.IsTupleType():
|
||||
newVal = TupleVal(elems)
|
||||
default:
|
||||
panic("unknown sequence type") // should never happen because of the case we are in
|
||||
}
|
||||
}
|
||||
|
||||
case ty.IsMapType():
|
||||
l := val.LengthInt()
|
||||
switch l {
|
||||
case 0:
|
||||
// No deep transform for an empty map
|
||||
newVal = val
|
||||
default:
|
||||
elems := make(map[string]Value)
|
||||
for it := val.ElementIterator(); it.Next(); {
|
||||
kv, ev := it.Element()
|
||||
path := append(path, IndexStep{
|
||||
Key: kv,
|
||||
})
|
||||
newEv, err := transform(path, ev, cb)
|
||||
if err != nil {
|
||||
return DynamicVal, err
|
||||
}
|
||||
elems[kv.AsString()] = newEv
|
||||
}
|
||||
newVal = MapVal(elems)
|
||||
}
|
||||
|
||||
case ty.IsObjectType():
|
||||
switch {
|
||||
case ty.Equals(EmptyObject):
|
||||
// No deep transform for an empty object
|
||||
newVal = val
|
||||
default:
|
||||
atys := ty.AttributeTypes()
|
||||
newAVs := make(map[string]Value)
|
||||
for name := range atys {
|
||||
av := val.GetAttr(name)
|
||||
path := append(path, GetAttrStep{
|
||||
Name: name,
|
||||
})
|
||||
newAV, err := transform(path, av, cb)
|
||||
if err != nil {
|
||||
return DynamicVal, err
|
||||
}
|
||||
newAVs[name] = newAV
|
||||
}
|
||||
newVal = ObjectVal(newAVs)
|
||||
}
|
||||
|
||||
default:
|
||||
newVal = val
|
||||
}
|
||||
|
||||
return cb(path, newVal)
|
||||
}
|
||||
Loading…
Reference in new issue