add vendor

This commit is contained in:
Malar Invention
2022-04-03 09:37:16 +05:30
parent f96ba5f172
commit 00ebcd295e
2339 changed files with 705854 additions and 0 deletions

View File

@ -0,0 +1,78 @@
package stdlib
import (
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
var NotFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "val",
Type: cty.Bool,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return args[0].Not(), nil
},
})
var AndFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "a",
Type: cty.Bool,
AllowDynamicType: true,
AllowMarked: true,
},
{
Name: "b",
Type: cty.Bool,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return args[0].And(args[1]), nil
},
})
var OrFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "a",
Type: cty.Bool,
AllowDynamicType: true,
AllowMarked: true,
},
{
Name: "b",
Type: cty.Bool,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return args[0].Or(args[1]), nil
},
})
// Not returns the logical complement of the given boolean value.
func Not(num cty.Value) (cty.Value, error) {
return NotFunc.Call([]cty.Value{num})
}
// And returns true if and only if both of the given boolean values are true.
func And(a, b cty.Value) (cty.Value, error) {
return AndFunc.Call([]cty.Value{a, b})
}
// Or returns true if either of the given boolean values are true.
func Or(a, b cty.Value) (cty.Value, error) {
return OrFunc.Call([]cty.Value{a, b})
}

View File

@ -0,0 +1,112 @@
package stdlib
import (
"fmt"
"reflect"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/gocty"
)
// Bytes is a capsule type that can be used with the binary functions to
// support applications that need to support raw buffers in addition to
// UTF-8 strings.
var Bytes = cty.Capsule("bytes", reflect.TypeOf([]byte(nil)))
// BytesVal creates a new Bytes value from the given buffer, which must be
// non-nil or this function will panic.
//
// Once a byte slice has been wrapped in a Bytes capsule, its underlying array
// must be considered immutable.
func BytesVal(buf []byte) cty.Value {
if buf == nil {
panic("can't make Bytes value from nil slice")
}
return cty.CapsuleVal(Bytes, &buf)
}
// BytesLen is a Function that returns the length of the buffer encapsulated
// in a Bytes value.
var BytesLenFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "buf",
Type: Bytes,
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
bufPtr := args[0].EncapsulatedValue().(*[]byte)
return cty.NumberIntVal(int64(len(*bufPtr))), nil
},
})
// BytesSlice is a Function that returns a slice of the given Bytes value.
var BytesSliceFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "buf",
Type: Bytes,
AllowDynamicType: true,
},
{
Name: "offset",
Type: cty.Number,
AllowDynamicType: true,
},
{
Name: "length",
Type: cty.Number,
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(Bytes),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
bufPtr := args[0].EncapsulatedValue().(*[]byte)
var offset, length int
var err error
err = gocty.FromCtyValue(args[1], &offset)
if err != nil {
return cty.NilVal, err
}
err = gocty.FromCtyValue(args[2], &length)
if err != nil {
return cty.NilVal, err
}
if offset < 0 || length < 0 {
return cty.NilVal, fmt.Errorf("offset and length must be non-negative")
}
if offset > len(*bufPtr) {
return cty.NilVal, fmt.Errorf(
"offset %d is greater than total buffer length %d",
offset, len(*bufPtr),
)
}
end := offset + length
if end > len(*bufPtr) {
return cty.NilVal, fmt.Errorf(
"offset %d + length %d is greater than total buffer length %d",
offset, length, len(*bufPtr),
)
}
return BytesVal((*bufPtr)[offset:end]), nil
},
})
func BytesLen(buf cty.Value) (cty.Value, error) {
return BytesLenFunc.Call([]cty.Value{buf})
}
func BytesSlice(buf cty.Value, offset cty.Value, length cty.Value) (cty.Value, error) {
return BytesSliceFunc.Call([]cty.Value{buf, offset, length})
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,87 @@
package stdlib
import (
"strconv"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/function"
)
// MakeToFunc constructs a "to..." function, like "tostring", which converts
// its argument to a specific type or type kind.
//
// The given type wantTy can be any type constraint that cty's "convert" package
// would accept. In particular, this means that you can pass
// cty.List(cty.DynamicPseudoType) to mean "list of any single type", which
// will then cause cty to attempt to unify all of the element types when given
// a tuple.
func MakeToFunc(wantTy cty.Type) function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "v",
// We use DynamicPseudoType rather than wantTy here so that
// all values will pass through the function API verbatim and
// we can handle the conversion logic within the Type and
// Impl functions. This allows us to customize the error
// messages to be more appropriate for an explicit type
// conversion, whereas the cty function system produces
// messages aimed at _implicit_ type conversions.
Type: cty.DynamicPseudoType,
AllowNull: true,
},
},
Type: func(args []cty.Value) (cty.Type, error) {
gotTy := args[0].Type()
if gotTy.Equals(wantTy) {
return wantTy, nil
}
conv := convert.GetConversionUnsafe(args[0].Type(), wantTy)
if conv == nil {
// We'll use some specialized errors for some trickier cases,
// but most we can handle in a simple way.
switch {
case gotTy.IsTupleType() && wantTy.IsTupleType():
return cty.NilType, function.NewArgErrorf(0, "incompatible tuple type for conversion: %s", convert.MismatchMessage(gotTy, wantTy))
case gotTy.IsObjectType() && wantTy.IsObjectType():
return cty.NilType, function.NewArgErrorf(0, "incompatible object type for conversion: %s", convert.MismatchMessage(gotTy, wantTy))
default:
return cty.NilType, function.NewArgErrorf(0, "cannot convert %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint())
}
}
// If a conversion is available then everything is fine.
return wantTy, nil
},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
// We didn't set "AllowUnknown" on our argument, so it is guaranteed
// to be known here but may still be null.
ret, err := convert.Convert(args[0], retType)
if err != nil {
// Because we used GetConversionUnsafe above, conversion can
// still potentially fail in here. For example, if the user
// asks to convert the string "a" to bool then we'll
// optimistically permit it during type checking but fail here
// once we note that the value isn't either "true" or "false".
gotTy := args[0].Type()
switch {
case gotTy == cty.String && wantTy == cty.Bool:
what := "string"
if !args[0].IsNull() {
what = strconv.Quote(args[0].AsString())
}
return cty.NilVal, function.NewArgErrorf(0, `cannot convert %s to bool; only the strings "true" or "false" are allowed`, what)
case gotTy == cty.String && wantTy == cty.Number:
what := "string"
if !args[0].IsNull() {
what = strconv.Quote(args[0].AsString())
}
return cty.NilVal, function.NewArgErrorf(0, `cannot convert %s to number; given string must be a decimal representation of a number`, what)
default:
return cty.NilVal, function.NewArgErrorf(0, "cannot convert %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint())
}
}
return ret, nil
},
})
}

View File

@ -0,0 +1,102 @@
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, csvError(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, csvError(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})
}
func csvError(err error) error {
switch err := err.(type) {
case *csv.ParseError:
return fmt.Errorf("CSV parse error on line %d: %w", err.Line, err.Err)
default:
return err
}
}

View File

@ -0,0 +1,434 @@
package stdlib
import (
"bufio"
"bytes"
"fmt"
"strings"
"time"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
var FormatDateFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "format",
Type: cty.String,
},
{
Name: "time",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
formatStr := args[0].AsString()
timeStr := args[1].AsString()
t, err := parseTimestamp(timeStr)
if err != nil {
return cty.DynamicVal, function.NewArgError(1, err)
}
var buf bytes.Buffer
sc := bufio.NewScanner(strings.NewReader(formatStr))
sc.Split(splitDateFormat)
const esc = '\''
for sc.Scan() {
tok := sc.Bytes()
// The leading byte signals the token type
switch {
case tok[0] == esc:
if tok[len(tok)-1] != esc || len(tok) == 1 {
return cty.DynamicVal, function.NewArgErrorf(0, "unterminated literal '")
}
if len(tok) == 2 {
// Must be a single escaped quote, ''
buf.WriteByte(esc)
} else {
// The content (until a closing esc) is printed out verbatim
// except that we must un-double any double-esc escapes in
// the middle of the string.
raw := tok[1 : len(tok)-1]
for i := 0; i < len(raw); i++ {
buf.WriteByte(raw[i])
if raw[i] == esc {
i++ // skip the escaped quote
}
}
}
case startsDateFormatVerb(tok[0]):
switch tok[0] {
case 'Y':
y := t.Year()
switch len(tok) {
case 2:
fmt.Fprintf(&buf, "%02d", y%100)
case 4:
fmt.Fprintf(&buf, "%04d", y)
default:
return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: year must either be \"YY\" or \"YYYY\"", tok)
}
case 'M':
m := t.Month()
switch len(tok) {
case 1:
fmt.Fprintf(&buf, "%d", m)
case 2:
fmt.Fprintf(&buf, "%02d", m)
case 3:
buf.WriteString(m.String()[:3])
case 4:
buf.WriteString(m.String())
default:
return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: month must be \"M\", \"MM\", \"MMM\", or \"MMMM\"", tok)
}
case 'D':
d := t.Day()
switch len(tok) {
case 1:
fmt.Fprintf(&buf, "%d", d)
case 2:
fmt.Fprintf(&buf, "%02d", d)
default:
return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: day of month must either be \"D\" or \"DD\"", tok)
}
case 'E':
d := t.Weekday()
switch len(tok) {
case 3:
buf.WriteString(d.String()[:3])
case 4:
buf.WriteString(d.String())
default:
return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: day of week must either be \"EEE\" or \"EEEE\"", tok)
}
case 'h':
h := t.Hour()
switch len(tok) {
case 1:
fmt.Fprintf(&buf, "%d", h)
case 2:
fmt.Fprintf(&buf, "%02d", h)
default:
return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: 24-hour must either be \"h\" or \"hh\"", tok)
}
case 'H':
h := t.Hour() % 12
if h == 0 {
h = 12
}
switch len(tok) {
case 1:
fmt.Fprintf(&buf, "%d", h)
case 2:
fmt.Fprintf(&buf, "%02d", h)
default:
return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: 12-hour must either be \"H\" or \"HH\"", tok)
}
case 'A', 'a':
if len(tok) != 2 {
return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: must be \"%s%s\"", tok, tok[0:1], tok[0:1])
}
upper := tok[0] == 'A'
switch t.Hour() / 12 {
case 0:
if upper {
buf.WriteString("AM")
} else {
buf.WriteString("am")
}
case 1:
if upper {
buf.WriteString("PM")
} else {
buf.WriteString("pm")
}
}
case 'm':
m := t.Minute()
switch len(tok) {
case 1:
fmt.Fprintf(&buf, "%d", m)
case 2:
fmt.Fprintf(&buf, "%02d", m)
default:
return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: minute must either be \"m\" or \"mm\"", tok)
}
case 's':
s := t.Second()
switch len(tok) {
case 1:
fmt.Fprintf(&buf, "%d", s)
case 2:
fmt.Fprintf(&buf, "%02d", s)
default:
return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: second must either be \"s\" or \"ss\"", tok)
}
case 'Z':
// We'll just lean on Go's own formatter for this one, since
// the necessary information is unexported.
switch len(tok) {
case 1:
buf.WriteString(t.Format("Z07:00"))
case 3:
str := t.Format("-0700")
switch str {
case "+0000":
buf.WriteString("UTC")
default:
buf.WriteString(str)
}
case 4:
buf.WriteString(t.Format("-0700"))
case 5:
buf.WriteString(t.Format("-07:00"))
default:
return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q: timezone must be Z, ZZZZ, or ZZZZZ", tok)
}
default:
return cty.DynamicVal, function.NewArgErrorf(0, "invalid date format verb %q", tok)
}
default:
// Any other starting character indicates a literal sequence
buf.Write(tok)
}
}
return cty.StringVal(buf.String()), nil
},
})
// TimeAddFunc is a function that adds a duration to a timestamp, returning a new timestamp.
var TimeAddFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "timestamp",
Type: cty.String,
},
{
Name: "duration",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
ts, err := parseTimestamp(args[0].AsString())
if err != nil {
return cty.UnknownVal(cty.String), err
}
duration, err := time.ParseDuration(args[1].AsString())
if err != nil {
return cty.UnknownVal(cty.String), err
}
return cty.StringVal(ts.Add(duration).Format(time.RFC3339)), nil
},
})
// FormatDate reformats a timestamp given in RFC3339 syntax into another time
// syntax defined by a given format string.
//
// The format string uses letter mnemonics to represent portions of the
// timestamp, with repetition signifying length variants of each portion.
// Single quote characters ' can be used to quote sequences of literal letters
// that should not be interpreted as formatting mnemonics.
//
// The full set of supported mnemonic sequences is listed below:
//
// YY Year modulo 100 zero-padded to two digits, like "06".
// YYYY Four (or more) digit year, like "2006".
// M Month number, like "1" for January.
// MM Month number zero-padded to two digits, like "01".
// MMM English month name abbreviated to three letters, like "Jan".
// MMMM English month name unabbreviated, like "January".
// D Day of month number, like "2".
// DD Day of month number zero-padded to two digits, like "02".
// EEE English day of week name abbreviated to three letters, like "Mon".
// EEEE English day of week name unabbreviated, like "Monday".
// h 24-hour number, like "2".
// hh 24-hour number zero-padded to two digits, like "02".
// H 12-hour number, like "2".
// HH 12-hour number zero-padded to two digits, like "02".
// AA Hour AM/PM marker in uppercase, like "AM".
// aa Hour AM/PM marker in lowercase, like "am".
// m Minute within hour, like "5".
// mm Minute within hour zero-padded to two digits, like "05".
// s Second within minute, like "9".
// ss Second within minute zero-padded to two digits, like "09".
// ZZZZ Timezone offset with just sign and digit, like "-0800".
// ZZZZZ Timezone offset with colon separating hours and minutes, like "-08:00".
// Z Like ZZZZZ but with a special case "Z" for UTC.
// ZZZ Like ZZZZ but with a special case "UTC" for UTC.
//
// The format syntax is optimized mainly for generating machine-oriented
// timestamps rather than human-oriented timestamps; the English language
// portions of the output reflect the use of English names in a number of
// machine-readable date formatting standards. For presentation to humans,
// a locale-aware time formatter (not included in this package) is a better
// choice.
//
// The format syntax is not compatible with that of any other language, but
// is optimized so that patterns for common standard date formats can be
// recognized quickly even by a reader unfamiliar with the format syntax.
func FormatDate(format cty.Value, timestamp cty.Value) (cty.Value, error) {
return FormatDateFunc.Call([]cty.Value{format, timestamp})
}
func parseTimestamp(ts string) (time.Time, error) {
t, err := time.Parse(time.RFC3339, ts)
if err != nil {
switch err := err.(type) {
case *time.ParseError:
// If err is s time.ParseError then its string representation is not
// appropriate since it relies on details of Go's strange date format
// representation, which a caller of our functions is not expected
// to be familiar with.
//
// Therefore we do some light transformation to get a more suitable
// error that should make more sense to our callers. These are
// still not awesome error messages, but at least they refer to
// the timestamp portions by name rather than by Go's example
// values.
if err.LayoutElem == "" && err.ValueElem == "" && err.Message != "" {
// For some reason err.Message is populated with a ": " prefix
// by the time package.
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp%s", err.Message)
}
var what string
switch err.LayoutElem {
case "2006":
what = "year"
case "01":
what = "month"
case "02":
what = "day of month"
case "15":
what = "hour"
case "04":
what = "minute"
case "05":
what = "second"
case "Z07:00":
what = "UTC offset"
case "T":
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: missing required time introducer 'T'")
case ":", "-":
if err.ValueElem == "" {
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string where %q is expected", err.LayoutElem)
} else {
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: found %q where %q is expected", err.ValueElem, err.LayoutElem)
}
default:
// Should never get here, because time.RFC3339 includes only the
// above portions, but since that might change in future we'll
// be robust here.
what = "timestamp segment"
}
if err.ValueElem == "" {
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string before %s", what)
} else {
return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: cannot use %q as %s", err.ValueElem, what)
}
}
return time.Time{}, err
}
return t, nil
}
// splitDataFormat is a bufio.SplitFunc used to tokenize a date format.
func splitDateFormat(data []byte, atEOF bool) (advance int, token []byte, err error) {
if len(data) == 0 {
return 0, nil, nil
}
const esc = '\''
switch {
case data[0] == esc:
// If we have another quote immediately after then this is a single
// escaped escape.
if len(data) > 1 && data[1] == esc {
return 2, data[:2], nil
}
// Beginning of quoted sequence, so we will seek forward until we find
// the closing quote, ignoring escaped quotes along the way.
for i := 1; i < len(data); i++ {
if data[i] == esc {
if (i + 1) == len(data) {
if atEOF {
// We have a closing quote and are at the end of our input
return len(data), data, nil
} else {
// We need at least one more byte to decide if this is an
// escape or a terminator.
return 0, nil, nil
}
}
if data[i+1] == esc {
i++ // doubled-up quotes are an escape sequence
continue
}
// We've found the closing quote
return i + 1, data[:i+1], nil
}
}
// If we fall out here then we need more bytes to find the end,
// unless we're already at the end with an unclosed quote.
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
case startsDateFormatVerb(data[0]):
rep := data[0]
for i := 1; i < len(data); i++ {
if data[i] != rep {
return i, data[:i], nil
}
}
if atEOF {
return len(data), data, nil
}
// We need more data to decide if we've found the end
return 0, nil, nil
default:
for i := 1; i < len(data); i++ {
if data[i] == esc || startsDateFormatVerb(data[i]) {
return i, data[:i], nil
}
}
// We might not actually be at the end of a literal sequence,
// but that doesn't matter since we'll concat them back together
// anyway.
return len(data), data, nil
}
}
func startsDateFormatVerb(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
}
// TimeAdd adds a duration to a timestamp, returning a new timestamp.
//
// In the HCL language, timestamps are conventionally represented as
// strings using RFC 3339 "Date and Time format" syntax. Timeadd requires
// the timestamp argument to be a string conforming to this syntax.
//
// `duration` is a string representation of a time difference, consisting of
// sequences of number and unit pairs, like `"1.5h"` or `1h30m`. The accepted
// units are `ns`, `us` (or `µs`), `"ms"`, `"s"`, `"m"`, and `"h"`. The first
// number may be negative to indicate a negative duration, like `"-2h5m"`.
//
// The result is a string, also in RFC 3339 format, representing the result
// of adding the given direction to the given timestamp.
func TimeAdd(timestamp cty.Value, duration cty.Value) (cty.Value, error) {
return TimeAddFunc.Call([]cty.Value{timestamp, duration})
}

View File

@ -0,0 +1,13 @@
// Package stdlib is a collection of cty functions that are expected to be
// generally useful, and are thus factored out into this shared library in
// the hope that cty-using applications will have consistent behavior when
// using these functions.
//
// See the parent package "function" for more information on the purpose
// and usage of cty functions.
//
// This package contains both Go functions, which provide convenient access
// to call the functions from Go code, and the Function objects themselves.
// The latter follow the naming scheme of appending "Func" to the end of
// the function name.
package stdlib

View File

@ -0,0 +1,519 @@
package stdlib
import (
"bytes"
"fmt"
"math/big"
"strings"
"github.com/apparentlymart/go-textseg/v13/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))
unknowns := make([]bool, len(args))
for i, arg := range args {
argTy := arg.Type()
switch {
case (argTy.IsListType() || argTy.IsSetType() || argTy.IsTupleType()) && !arg.IsNull():
if !argTy.IsTupleType() && !(arg.IsKnown() && arg.Length().IsKnown()) {
// We can't iterate this one at all yet then, so we can't
// yet produce a result.
unknowns[i] = true
continue
}
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,
)
}
}
if !arg.IsKnown() {
// We allowed an unknown tuple value to fall through in
// our initial check above so that we'd be able to run
// the above error checks against it, but we still can't
// iterate it if the checks pass.
unknowns[i] = true
continue
}
iterators[i] = arg.ElementIterator()
case arg == cty.DynamicVal:
unknowns[i] = true
default:
singleVals[i] = arg
}
}
for _, isUnk := range unknowns {
if isUnk {
return cty.UnknownVal(retType), nil
}
}
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:]
}

View File

@ -0,0 +1,374 @@
// 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"
"github.com/zclconf/go-cty/cty/function"
)
// line 21 "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 20 "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
highestArgIdx := 0 // zero means "none", since arg numbers are 1-based
// line 159 "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 123 "format_fsm.go"
{
cs = formatfsm_start
}
// line 128 "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 31 "format_fsm.rl"
verb = formatVerb{
ArgNum: nextArg,
Prec: -1,
Width: -1,
}
ts = p
case 1:
// line 40 "format_fsm.rl"
buf.WriteByte(data[p])
case 4:
// line 51 "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 58 "format_fsm.rl"
verb.Sharp = true
case 6:
// line 61 "format_fsm.rl"
verb.Zero = true
case 7:
// line 64 "format_fsm.rl"
verb.Minus = true
case 8:
// line 67 "format_fsm.rl"
verb.Plus = true
case 9:
// line 70 "format_fsm.rl"
verb.Space = true
case 10:
// line 74 "format_fsm.rl"
verb.ArgNum = 0
case 11:
// line 77 "format_fsm.rl"
verb.ArgNum = (10 * verb.ArgNum) + (int(data[p]) - '0')
case 12:
// line 81 "format_fsm.rl"
verb.HasWidth = true
case 13:
// line 84 "format_fsm.rl"
verb.Width = 0
case 14:
// line 87 "format_fsm.rl"
verb.Width = (10 * verb.Width) + (int(data[p]) - '0')
case 15:
// line 91 "format_fsm.rl"
verb.HasPrec = true
case 16:
// line 94 "format_fsm.rl"
verb.Prec = 0
case 17:
// line 97 "format_fsm.rl"
verb.Prec = (10 * verb.Prec) + (int(data[p]) - '0')
case 18:
// line 101 "format_fsm.rl"
verb.Mode = rune(data[p])
te = p + 1
verb.Raw = data[ts:te]
verb.Offset = ts
if verb.ArgNum > highestArgIdx {
highestArgIdx = verb.ArgNum
}
err := formatAppend(&verb, &buf, a)
if err != nil {
return buf.String(), err
}
nextArg = verb.ArgNum + 1
// line 330 "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 44 "format_fsm.rl"
case 3:
// line 47 "format_fsm.rl"
return buf.String(), fmt.Errorf("invalid format string starting at offset %d", p)
case 4:
// line 51 "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 369 "format_fsm.go"
}
}
}
_out:
{
}
}
// line 177 "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 %d", p)
}
if highestArgIdx < len(a) {
// Extraneous args are an error, to more easily detect mistakes
firstBad := highestArgIdx + 1
if highestArgIdx == 0 {
// Custom error message for this case
return buf.String(), function.NewArgErrorf(firstBad, "too many arguments; no verbs in format string")
}
return buf.String(), function.NewArgErrorf(firstBad, "too many arguments; only %d used by format string", highestArgIdx)
}
return buf.String(), nil
}

View File

@ -0,0 +1,198 @@
// 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"
"github.com/zclconf/go-cty/cty/function"
)
%%{
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
highestArgIdx := 0 // zero means "none", since arg numbers are 1-based
%%{
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
if verb.ArgNum > highestArgIdx {
highestArgIdx = verb.ArgNum
}
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 %d", p)
}
if highestArgIdx < len(a) {
// Extraneous args are an error, to more easily detect mistakes
firstBad := highestArgIdx+1
if highestArgIdx == 0 {
// Custom error message for this case
return buf.String(), function.NewArgErrorf(firstBad, "too many arguments; no verbs in format string")
}
return buf.String(), function.NewArgErrorf(firstBad, "too many arguments; only %d used by format string", highestArgIdx)
}
return buf.String(), nil
}

View File

@ -0,0 +1,107 @@
package stdlib
import (
"fmt"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/function"
)
var EqualFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "a",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
{
Name: "b",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return args[0].Equals(args[1]), nil
},
})
var NotEqualFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "a",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
{
Name: "b",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return args[0].Equals(args[1]).Not(), nil
},
})
var CoalesceFunc = function.New(&function.Spec{
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "vals",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
argTypes := make([]cty.Type, len(args))
for i, val := range args {
argTypes[i] = val.Type()
}
retType, _ := convert.UnifyUnsafe(argTypes)
if retType == cty.NilType {
return cty.NilType, fmt.Errorf("all arguments must have the same type")
}
return retType, nil
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
for _, argVal := range args {
if !argVal.IsKnown() {
return cty.UnknownVal(retType), nil
}
if argVal.IsNull() {
continue
}
return convert.Convert(argVal, retType)
}
return cty.NilVal, fmt.Errorf("no non-null arguments")
},
})
// Equal determines whether the two given values are equal, returning a
// bool value.
func Equal(a cty.Value, b cty.Value) (cty.Value, error) {
return EqualFunc.Call([]cty.Value{a, b})
}
// NotEqual is the opposite of Equal.
func NotEqual(a cty.Value, b cty.Value) (cty.Value, error) {
return NotEqualFunc.Call([]cty.Value{a, b})
}
// Coalesce returns the first of the given arguments that is not null. If
// all arguments are null, an error is produced.
func Coalesce(vals ...cty.Value) (cty.Value, error) {
return CoalesceFunc.Call(vals)
}

View File

@ -0,0 +1,77 @@
package stdlib
import (
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/json"
)
var JSONEncodeFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "val",
Type: cty.DynamicPseudoType,
AllowDynamicType: true,
AllowNull: true,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
val := args[0]
if !val.IsWhollyKnown() {
// We can't serialize unknowns, so if the value is unknown or
// contains any _nested_ unknowns then our result must be
// unknown.
return cty.UnknownVal(retType), nil
}
if val.IsNull() {
return cty.StringVal("null"), nil
}
buf, err := json.Marshal(val, val.Type())
if err != nil {
return cty.NilVal, err
}
return cty.StringVal(string(buf)), nil
},
})
var JSONDecodeFunc = 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
}
buf := []byte(str.AsString())
return json.ImpliedType(buf)
},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
buf := []byte(args[0].AsString())
return json.Unmarshal(buf, retType)
},
})
// JSONEncode returns a JSON serialization of the given value.
func JSONEncode(val cty.Value) (cty.Value, error) {
return JSONEncodeFunc.Call([]cty.Value{val})
}
// JSONDecode parses the given JSON string and, if it is valid, returns the
// value it represents.
//
// Note that applying JSONDecode to the result of JSONEncode may not produce
// an identically-typed result, since JSON encoding is lossy for cty Types.
// The resulting value will consist only of primitive types, object types, and
// tuple types.
func JSONDecode(str cty.Value) (cty.Value, error) {
return JSONDecodeFunc.Call([]cty.Value{str})
}

View File

@ -0,0 +1,667 @@
package stdlib
import (
"fmt"
"math"
"math/big"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/gocty"
)
var AbsoluteFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "num",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return args[0].Absolute(), nil
},
})
var AddFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "a",
Type: cty.Number,
AllowDynamicType: true,
},
{
Name: "b",
Type: cty.Number,
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
// big.Float.Add can panic if the input values are opposing infinities,
// so we must catch that here in order to remain within
// the cty Function abstraction.
defer func() {
if r := recover(); r != nil {
if _, ok := r.(big.ErrNaN); ok {
ret = cty.NilVal
err = fmt.Errorf("can't compute sum of opposing infinities")
} else {
// not a panic we recognize
panic(r)
}
}
}()
return args[0].Add(args[1]), nil
},
})
var SubtractFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "a",
Type: cty.Number,
AllowDynamicType: true,
},
{
Name: "b",
Type: cty.Number,
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
// big.Float.Sub can panic if the input values are infinities,
// so we must catch that here in order to remain within
// the cty Function abstraction.
defer func() {
if r := recover(); r != nil {
if _, ok := r.(big.ErrNaN); ok {
ret = cty.NilVal
err = fmt.Errorf("can't subtract infinity from itself")
} else {
// not a panic we recognize
panic(r)
}
}
}()
return args[0].Subtract(args[1]), nil
},
})
var MultiplyFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "a",
Type: cty.Number,
AllowDynamicType: true,
},
{
Name: "b",
Type: cty.Number,
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
// big.Float.Mul can panic if the input values are both zero or both
// infinity, so we must catch that here in order to remain within
// the cty Function abstraction.
defer func() {
if r := recover(); r != nil {
if _, ok := r.(big.ErrNaN); ok {
ret = cty.NilVal
err = fmt.Errorf("can't multiply zero by infinity")
} else {
// not a panic we recognize
panic(r)
}
}
}()
return args[0].Multiply(args[1]), nil
},
})
var DivideFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "a",
Type: cty.Number,
AllowDynamicType: true,
},
{
Name: "b",
Type: cty.Number,
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
// big.Float.Quo can panic if the input values are both zero or both
// infinity, so we must catch that here in order to remain within
// the cty Function abstraction.
defer func() {
if r := recover(); r != nil {
if _, ok := r.(big.ErrNaN); ok {
ret = cty.NilVal
err = fmt.Errorf("can't divide zero by zero or infinity by infinity")
} else {
// not a panic we recognize
panic(r)
}
}
}()
return args[0].Divide(args[1]), nil
},
})
var ModuloFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "a",
Type: cty.Number,
AllowDynamicType: true,
},
{
Name: "b",
Type: cty.Number,
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
// big.Float.Mul can panic if the input values are both zero or both
// infinity, so we must catch that here in order to remain within
// the cty Function abstraction.
defer func() {
if r := recover(); r != nil {
if _, ok := r.(big.ErrNaN); ok {
ret = cty.NilVal
err = fmt.Errorf("can't use modulo with zero and infinity")
} else {
// not a panic we recognize
panic(r)
}
}
}()
return args[0].Modulo(args[1]), nil
},
})
var GreaterThanFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "a",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
{
Name: "b",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return args[0].GreaterThan(args[1]), nil
},
})
var GreaterThanOrEqualToFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "a",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
{
Name: "b",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return args[0].GreaterThanOrEqualTo(args[1]), nil
},
})
var LessThanFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "a",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
{
Name: "b",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return args[0].LessThan(args[1]), nil
},
})
var LessThanOrEqualToFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "a",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
{
Name: "b",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return args[0].LessThanOrEqualTo(args[1]), nil
},
})
var NegateFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "num",
Type: cty.Number,
AllowDynamicType: true,
AllowMarked: true,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return args[0].Negate(), nil
},
})
var MinFunc = function.New(&function.Spec{
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "numbers",
Type: cty.Number,
AllowDynamicType: true,
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
if len(args) == 0 {
return cty.NilVal, fmt.Errorf("must pass at least one number")
}
min := cty.PositiveInfinity
for _, num := range args {
if num.LessThan(min).True() {
min = num
}
}
return min, nil
},
})
var MaxFunc = function.New(&function.Spec{
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "numbers",
Type: cty.Number,
AllowDynamicType: true,
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
if len(args) == 0 {
return cty.NilVal, fmt.Errorf("must pass at least one number")
}
max := cty.NegativeInfinity
for _, num := range args {
if num.GreaterThan(max).True() {
max = num
}
}
return max, nil
},
})
var IntFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "num",
Type: cty.Number,
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
bf := args[0].AsBigFloat()
if bf.IsInt() {
return args[0], nil
}
bi, _ := bf.Int(nil)
bf = (&big.Float{}).SetInt(bi)
return cty.NumberVal(bf), nil
},
})
// CeilFunc is a function that returns the closest whole number greater
// than or equal to the given value.
var CeilFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "num",
Type: cty.Number,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
f := args[0].AsBigFloat()
if f.IsInf() {
return cty.NumberVal(f), nil
}
i, acc := f.Int(nil)
switch acc {
case big.Exact, big.Above:
// Done.
case big.Below:
i.Add(i, big.NewInt(1))
}
return cty.NumberVal(f.SetInt(i)), nil
},
})
// FloorFunc is a function that returns the closest whole number lesser
// than or equal to the given value.
var FloorFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "num",
Type: cty.Number,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
f := args[0].AsBigFloat()
if f.IsInf() {
return cty.NumberVal(f), nil
}
i, acc := f.Int(nil)
switch acc {
case big.Exact, big.Below:
// Done.
case big.Above:
i.Sub(i, big.NewInt(1))
}
return cty.NumberVal(f.SetInt(i)), nil
},
})
// LogFunc is a function that returns the logarithm of a given number in a given base.
var LogFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "num",
Type: cty.Number,
},
{
Name: "base",
Type: cty.Number,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
var num float64
if err := gocty.FromCtyValue(args[0], &num); err != nil {
return cty.UnknownVal(cty.String), err
}
var base float64
if err := gocty.FromCtyValue(args[1], &base); err != nil {
return cty.UnknownVal(cty.String), err
}
return cty.NumberFloatVal(math.Log(num) / math.Log(base)), nil
},
})
// PowFunc is a function that returns the logarithm of a given number in a given base.
var PowFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "num",
Type: cty.Number,
},
{
Name: "power",
Type: cty.Number,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
var num float64
if err := gocty.FromCtyValue(args[0], &num); err != nil {
return cty.UnknownVal(cty.String), err
}
var power float64
if err := gocty.FromCtyValue(args[1], &power); err != nil {
return cty.UnknownVal(cty.String), err
}
return cty.NumberFloatVal(math.Pow(num, power)), nil
},
})
// SignumFunc is a function that determines the sign of a number, returning a
// number between -1 and 1 to represent the sign..
var SignumFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "num",
Type: cty.Number,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
var num int
if err := gocty.FromCtyValue(args[0], &num); err != nil {
return cty.UnknownVal(cty.String), err
}
switch {
case num < 0:
return cty.NumberIntVal(-1), nil
case num > 0:
return cty.NumberIntVal(+1), nil
default:
return cty.NumberIntVal(0), nil
}
},
})
// ParseIntFunc is a function that parses a string argument and returns an integer of the specified base.
var ParseIntFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "number",
Type: cty.DynamicPseudoType,
},
{
Name: "base",
Type: cty.Number,
},
},
Type: func(args []cty.Value) (cty.Type, error) {
if !args[0].Type().Equals(cty.String) {
return cty.Number, function.NewArgErrorf(0, "first argument must be a string, not %s", args[0].Type().FriendlyName())
}
return cty.Number, nil
},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
var numstr string
var base int
var err error
if err = gocty.FromCtyValue(args[0], &numstr); err != nil {
return cty.UnknownVal(cty.String), function.NewArgError(0, err)
}
if err = gocty.FromCtyValue(args[1], &base); err != nil {
return cty.UnknownVal(cty.Number), function.NewArgError(1, err)
}
if base < 2 || base > 62 {
return cty.UnknownVal(cty.Number), function.NewArgErrorf(
1,
"base must be a whole number between 2 and 62 inclusive",
)
}
num, ok := (&big.Int{}).SetString(numstr, base)
if !ok {
return cty.UnknownVal(cty.Number), function.NewArgErrorf(
0,
"cannot parse %q as a base %d integer",
numstr,
base,
)
}
parsedNum := cty.NumberVal((&big.Float{}).SetInt(num))
return parsedNum, nil
},
})
// Absolute returns the magnitude of the given number, without its sign.
// That is, it turns negative values into positive values.
func Absolute(num cty.Value) (cty.Value, error) {
return AbsoluteFunc.Call([]cty.Value{num})
}
// Add returns the sum of the two given numbers.
func Add(a cty.Value, b cty.Value) (cty.Value, error) {
return AddFunc.Call([]cty.Value{a, b})
}
// Subtract returns the difference between the two given numbers.
func Subtract(a cty.Value, b cty.Value) (cty.Value, error) {
return SubtractFunc.Call([]cty.Value{a, b})
}
// Multiply returns the product of the two given numbers.
func Multiply(a cty.Value, b cty.Value) (cty.Value, error) {
return MultiplyFunc.Call([]cty.Value{a, b})
}
// Divide returns a divided by b, where both a and b are numbers.
func Divide(a cty.Value, b cty.Value) (cty.Value, error) {
return DivideFunc.Call([]cty.Value{a, b})
}
// Negate returns the given number multipled by -1.
func Negate(num cty.Value) (cty.Value, error) {
return NegateFunc.Call([]cty.Value{num})
}
// LessThan returns true if a is less than b.
func LessThan(a cty.Value, b cty.Value) (cty.Value, error) {
return LessThanFunc.Call([]cty.Value{a, b})
}
// LessThanOrEqualTo returns true if a is less than b.
func LessThanOrEqualTo(a cty.Value, b cty.Value) (cty.Value, error) {
return LessThanOrEqualToFunc.Call([]cty.Value{a, b})
}
// GreaterThan returns true if a is less than b.
func GreaterThan(a cty.Value, b cty.Value) (cty.Value, error) {
return GreaterThanFunc.Call([]cty.Value{a, b})
}
// GreaterThanOrEqualTo returns true if a is less than b.
func GreaterThanOrEqualTo(a cty.Value, b cty.Value) (cty.Value, error) {
return GreaterThanOrEqualToFunc.Call([]cty.Value{a, b})
}
// Modulo returns the remainder of a divided by b under integer division,
// where both a and b are numbers.
func Modulo(a cty.Value, b cty.Value) (cty.Value, error) {
return ModuloFunc.Call([]cty.Value{a, b})
}
// Min returns the minimum number from the given numbers.
func Min(numbers ...cty.Value) (cty.Value, error) {
return MinFunc.Call(numbers)
}
// Max returns the maximum number from the given numbers.
func Max(numbers ...cty.Value) (cty.Value, error) {
return MaxFunc.Call(numbers)
}
// Int removes the fractional component of the given number returning an
// integer representing the whole number component, rounding towards zero.
// For example, -1.5 becomes -1.
//
// If an infinity is passed to Int, an error is returned.
func Int(num cty.Value) (cty.Value, error) {
if num == cty.PositiveInfinity || num == cty.NegativeInfinity {
return cty.NilVal, fmt.Errorf("can't truncate infinity to an integer")
}
return IntFunc.Call([]cty.Value{num})
}
// Ceil returns the closest whole number greater than or equal to the given value.
func Ceil(num cty.Value) (cty.Value, error) {
return CeilFunc.Call([]cty.Value{num})
}
// Floor returns the closest whole number lesser than or equal to the given value.
func Floor(num cty.Value) (cty.Value, error) {
return FloorFunc.Call([]cty.Value{num})
}
// Log returns returns the logarithm of a given number in a given base.
func Log(num, base cty.Value) (cty.Value, error) {
return LogFunc.Call([]cty.Value{num, base})
}
// Pow returns the logarithm of a given number in a given base.
func Pow(num, power cty.Value) (cty.Value, error) {
return PowFunc.Call([]cty.Value{num, power})
}
// Signum determines the sign of a number, returning a number between -1 and
// 1 to represent the sign.
func Signum(num cty.Value) (cty.Value, error) {
return SignumFunc.Call([]cty.Value{num})
}
// ParseInt parses a string argument and returns an integer of the specified base.
func ParseInt(num cty.Value, base cty.Value) (cty.Value, error) {
return ParseIntFunc.Call([]cty.Value{num, base})
}

View File

@ -0,0 +1,233 @@
package stdlib
import (
"fmt"
"regexp"
resyntax "regexp/syntax"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
var RegexFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "pattern",
Type: cty.String,
},
{
Name: "string",
Type: cty.String,
},
},
Type: func(args []cty.Value) (cty.Type, error) {
if !args[0].IsKnown() {
// We can't predict our type without seeing our pattern
return cty.DynamicPseudoType, nil
}
retTy, err := regexPatternResultType(args[0].AsString())
if err != nil {
err = function.NewArgError(0, err)
}
return retTy, err
},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
if retType == cty.DynamicPseudoType {
return cty.DynamicVal, nil
}
re, err := regexp.Compile(args[0].AsString())
if err != nil {
// Should never happen, since we checked this in the Type function above.
return cty.NilVal, function.NewArgErrorf(0, "error parsing pattern: %s", err)
}
str := args[1].AsString()
captureIdxs := re.FindStringSubmatchIndex(str)
if captureIdxs == nil {
return cty.NilVal, fmt.Errorf("pattern did not match any part of the given string")
}
return regexPatternResult(re, str, captureIdxs, retType), nil
},
})
var RegexAllFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "pattern",
Type: cty.String,
},
{
Name: "string",
Type: cty.String,
},
},
Type: func(args []cty.Value) (cty.Type, error) {
if !args[0].IsKnown() {
// We can't predict our type without seeing our pattern,
// but we do know it'll always be a list of something.
return cty.List(cty.DynamicPseudoType), nil
}
retTy, err := regexPatternResultType(args[0].AsString())
if err != nil {
err = function.NewArgError(0, err)
}
return cty.List(retTy), err
},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
ety := retType.ElementType()
if ety == cty.DynamicPseudoType {
return cty.DynamicVal, nil
}
re, err := regexp.Compile(args[0].AsString())
if err != nil {
// Should never happen, since we checked this in the Type function above.
return cty.NilVal, function.NewArgErrorf(0, "error parsing pattern: %s", err)
}
str := args[1].AsString()
captureIdxsEach := re.FindAllStringSubmatchIndex(str, -1)
if len(captureIdxsEach) == 0 {
return cty.ListValEmpty(ety), nil
}
elems := make([]cty.Value, len(captureIdxsEach))
for i, captureIdxs := range captureIdxsEach {
elems[i] = regexPatternResult(re, str, captureIdxs, ety)
}
return cty.ListVal(elems), nil
},
})
// Regex is a function that extracts one or more substrings from a given
// string by applying a regular expression pattern, describing the first
// match.
//
// The return type depends on the composition of the capture groups (if any)
// in the pattern:
//
// - If there are no capture groups at all, the result is a single string
// representing the entire matched pattern.
// - If all of the capture groups are named, the result is an object whose
// keys are the named groups and whose values are their sub-matches, or
// null if a particular sub-group was inside another group that didn't
// match.
// - If none of the capture groups are named, the result is a tuple whose
// elements are the sub-groups in order and whose values are their
// sub-matches, or null if a particular sub-group was inside another group
// that didn't match.
// - It is invalid to use both named and un-named capture groups together in
// the same pattern.
//
// If the pattern doesn't match, this function returns an error. To test for
// a match, call RegexAll and check if the length of the result is greater
// than zero.
func Regex(pattern, str cty.Value) (cty.Value, error) {
return RegexFunc.Call([]cty.Value{pattern, str})
}
// RegexAll is similar to Regex but it finds all of the non-overlapping matches
// in the given string and returns a list of them.
//
// The result type is always a list, whose element type is deduced from the
// pattern in the same way as the return type for Regex is decided.
//
// If the pattern doesn't match at all, this function returns an empty list.
func RegexAll(pattern, str cty.Value) (cty.Value, error) {
return RegexAllFunc.Call([]cty.Value{pattern, str})
}
// regexPatternResultType parses the given regular expression pattern and
// returns the structural type that would be returned to represent its
// capture groups.
//
// Returns an error if parsing fails or if the pattern uses a mixture of
// named and unnamed capture groups, which is not permitted.
func regexPatternResultType(pattern string) (cty.Type, error) {
re, rawErr := regexp.Compile(pattern)
switch err := rawErr.(type) {
case *resyntax.Error:
return cty.NilType, fmt.Errorf("invalid regexp pattern: %s in %s", err.Code, err.Expr)
case error:
// Should never happen, since all regexp compile errors should
// be resyntax.Error, but just in case...
return cty.NilType, fmt.Errorf("error parsing pattern: %s", err)
}
allNames := re.SubexpNames()[1:]
var names []string
unnamed := 0
for _, name := range allNames {
if name == "" {
unnamed++
} else {
if names == nil {
names = make([]string, 0, len(allNames))
}
names = append(names, name)
}
}
switch {
case unnamed == 0 && len(names) == 0:
// If there are no capture groups at all then we'll return just a
// single string for the whole match.
return cty.String, nil
case unnamed > 0 && len(names) > 0:
return cty.NilType, fmt.Errorf("invalid regexp pattern: cannot mix both named and unnamed capture groups")
case unnamed > 0:
// For unnamed captures, we return a tuple of them all in order.
etys := make([]cty.Type, unnamed)
for i := range etys {
etys[i] = cty.String
}
return cty.Tuple(etys), nil
default:
// For named captures, we return an object using the capture names
// as keys.
atys := make(map[string]cty.Type, len(names))
for _, name := range names {
atys[name] = cty.String
}
return cty.Object(atys), nil
}
}
func regexPatternResult(re *regexp.Regexp, str string, captureIdxs []int, retType cty.Type) cty.Value {
switch {
case retType == cty.String:
start, end := captureIdxs[0], captureIdxs[1]
return cty.StringVal(str[start:end])
case retType.IsTupleType():
captureIdxs = captureIdxs[2:] // index 0 is the whole pattern span, which we ignore by skipping one pair
vals := make([]cty.Value, len(captureIdxs)/2)
for i := range vals {
start, end := captureIdxs[i*2], captureIdxs[i*2+1]
if start < 0 || end < 0 {
vals[i] = cty.NullVal(cty.String) // Did not match anything because containing group didn't match
continue
}
vals[i] = cty.StringVal(str[start:end])
}
return cty.TupleVal(vals)
case retType.IsObjectType():
captureIdxs = captureIdxs[2:] // index 0 is the whole pattern span, which we ignore by skipping one pair
vals := make(map[string]cty.Value, len(captureIdxs)/2)
names := re.SubexpNames()[1:]
for i, name := range names {
start, end := captureIdxs[i*2], captureIdxs[i*2+1]
if start < 0 || end < 0 {
vals[name] = cty.NullVal(cty.String) // Did not match anything because containing group didn't match
continue
}
vals[name] = cty.StringVal(str[start:end])
}
return cty.ObjectVal(vals)
default:
// Should never happen
panic(fmt.Sprintf("invalid return type %#v", retType))
}
}

View File

@ -0,0 +1,235 @@
package stdlib
import (
"fmt"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/function"
)
var ConcatFunc = function.New(&function.Spec{
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "seqs",
Type: cty.DynamicPseudoType,
AllowMarked: true,
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
if len(args) == 0 {
return cty.NilType, fmt.Errorf("at least one argument is required")
}
if args[0].Type().IsListType() {
// Possibly we're going to return a list, if all of our other
// args are also lists and we can find a common element type.
tys := make([]cty.Type, len(args))
for i, val := range args {
ty := val.Type()
if !ty.IsListType() {
tys = nil
break
}
tys[i] = ty
}
if tys != nil {
commonType, _ := convert.UnifyUnsafe(tys)
if commonType != cty.NilType {
return commonType, nil
}
}
}
etys := make([]cty.Type, 0, len(args))
for i, val := range args {
// Discard marks for nested values, as we only need to handle types
// and lengths.
val, _ := val.UnmarkDeep()
ety := val.Type()
switch {
case ety.IsTupleType():
etys = append(etys, ety.TupleElementTypes()...)
case ety.IsListType():
if !val.IsKnown() {
// We need to know the list to count its elements to
// build our tuple type, so any concat of an unknown
// list can't be typed yet.
return cty.DynamicPseudoType, nil
}
l := val.LengthInt()
subEty := ety.ElementType()
for j := 0; j < l; j++ {
etys = append(etys, subEty)
}
default:
return cty.NilType, function.NewArgErrorf(
i, "all arguments must be lists or tuples; got %s",
ety.FriendlyName(),
)
}
}
return cty.Tuple(etys), nil
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
switch {
case retType.IsListType():
// If retType is a list type then we know that all of the
// given values will be lists and that they will either be of
// retType or of something we can convert to retType.
vals := make([]cty.Value, 0, len(args))
var markses []cty.ValueMarks // remember any marked lists we find
for i, list := range args {
list, err = convert.Convert(list, retType)
if err != nil {
// Conversion might fail because we used UnifyUnsafe
// to choose our return type.
return cty.NilVal, function.NewArgError(i, err)
}
list, listMarks := list.Unmark()
if len(listMarks) > 0 {
markses = append(markses, listMarks)
}
it := list.ElementIterator()
for it.Next() {
_, v := it.Element()
vals = append(vals, v)
}
}
if len(vals) == 0 {
return cty.ListValEmpty(retType.ElementType()).WithMarks(markses...), nil
}
return cty.ListVal(vals).WithMarks(markses...), nil
case retType.IsTupleType():
// If retType is a tuple type then we could have a mixture of
// lists and tuples but we know they all have known values
// (because our params don't AllowUnknown) and we know that
// concatenating them all together will produce a tuple of
// retType because of the work we did in the Type function above.
vals := make([]cty.Value, 0, len(args))
var markses []cty.ValueMarks // remember any marked seqs we find
for _, seq := range args {
seq, seqMarks := seq.Unmark()
if len(seqMarks) > 0 {
markses = append(markses, seqMarks)
}
// Both lists and tuples support ElementIterator, so this is easy.
it := seq.ElementIterator()
for it.Next() {
_, v := it.Element()
vals = append(vals, v)
}
}
return cty.TupleVal(vals).WithMarks(markses...), nil
default:
// should never happen if Type is working correctly above
panic("unsupported return type")
}
},
})
var RangeFunc = function.New(&function.Spec{
VarParam: &function.Parameter{
Name: "params",
Type: cty.Number,
},
Type: function.StaticReturnType(cty.List(cty.Number)),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
var start, end, step cty.Value
switch len(args) {
case 1:
if args[0].LessThan(cty.Zero).True() {
start, end, step = cty.Zero, args[0], cty.NumberIntVal(-1)
} else {
start, end, step = cty.Zero, args[0], cty.NumberIntVal(1)
}
case 2:
if args[1].LessThan(args[0]).True() {
start, end, step = args[0], args[1], cty.NumberIntVal(-1)
} else {
start, end, step = args[0], args[1], cty.NumberIntVal(1)
}
case 3:
start, end, step = args[0], args[1], args[2]
default:
return cty.NilVal, fmt.Errorf("must have one, two, or three arguments")
}
var vals []cty.Value
if step == cty.Zero {
return cty.NilVal, function.NewArgErrorf(2, "step must not be zero")
}
down := step.LessThan(cty.Zero).True()
if down {
if end.GreaterThan(start).True() {
return cty.NilVal, function.NewArgErrorf(1, "end must be less than start when step is negative")
}
} else {
if end.LessThan(start).True() {
return cty.NilVal, function.NewArgErrorf(1, "end must be greater than start when step is positive")
}
}
num := start
for {
if down {
if num.LessThanOrEqualTo(end).True() {
break
}
} else {
if num.GreaterThanOrEqualTo(end).True() {
break
}
}
if len(vals) >= 1024 {
// Artificial limit to prevent bad arguments from consuming huge amounts of memory
return cty.NilVal, fmt.Errorf("more than 1024 values were generated; either decrease the difference between start and end or use a smaller step")
}
vals = append(vals, num)
num = num.Add(step)
}
if len(vals) == 0 {
return cty.ListValEmpty(cty.Number), nil
}
return cty.ListVal(vals), nil
},
})
// Concat takes one or more sequences (lists or tuples) and returns the single
// sequence that results from concatenating them together in order.
//
// If all of the given sequences are lists of the same element type then the
// result is a list of that type. Otherwise, the result is a of a tuple type
// constructed from the given sequence types.
func Concat(seqs ...cty.Value) (cty.Value, error) {
return ConcatFunc.Call(seqs)
}
// Range creates a list of numbers by starting from the given starting value,
// then adding the given step value until the result is greater than or
// equal to the given stopping value. Each intermediate result becomes an
// element in the resulting list.
//
// When all three parameters are set, the order is (start, end, step). If
// only two parameters are set, they are the start and end respectively and
// step defaults to 1. If only one argument is set, it gives the end value
// with start defaulting to 0 and step defaulting to 1.
//
// Because the resulting list must be fully buffered in memory, there is an
// artificial cap of 1024 elements, after which this function will return
// an error to avoid consuming unbounded amounts of memory. The Range function
// is primarily intended for creating small lists of indices to iterate over,
// so there should be no reason to generate huge lists with it.
func Range(params ...cty.Value) (cty.Value, error) {
return RangeFunc.Call(params)
}

View File

@ -0,0 +1,222 @@
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)
}, true),
})
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)
}, false),
})
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)
}, false),
})
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.SymmetricDifference(s2)
}, false),
})
// 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 {
ty := arg.Type().ElementType()
// Do not unify types for empty dynamic pseudo typed collections. These
// will always convert to any other concrete type.
if arg.IsKnown() && arg.LengthInt() == 0 && ty.Equals(cty.DynamicPseudoType) {
continue
}
etys = append(etys, ty)
}
// If all element types were skipped (due to being empty dynamic collections),
// the return type should also be a set of dynamic pseudo type.
if len(etys) == 0 {
return cty.Set(cty.DynamicPseudoType), nil
}
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, allowUnknowns bool) 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)
}
if !allowUnknowns && !first.IsWhollyKnown() {
// This set function can produce a correct result only when all
// elements are known, because eventually knowing the unknown
// values may cause the result to have fewer known elements, or
// might cause a result with no unknown elements at all to become
// one with a different length.
return cty.UnknownVal(retType), nil
}
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)
}
if !allowUnknowns && !arg.IsWhollyKnown() {
// (For the same reason as we did this check for "first" above.)
return cty.UnknownVal(retType), nil
}
argSet := arg.AsValueSet()
set = f(set, argSet)
}
return cty.SetValFromValueSet(set), nil
}
}

View File

@ -0,0 +1,546 @@
package stdlib
import (
"fmt"
"regexp"
"sort"
"strings"
"github.com/apparentlymart/go-textseg/v13/textseg"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/gocty"
)
var UpperFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
in := args[0].AsString()
out := strings.ToUpper(in)
return cty.StringVal(out), nil
},
})
var LowerFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
in := args[0].AsString()
out := strings.ToLower(in)
return cty.StringVal(out), nil
},
})
var ReverseFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
in := []byte(args[0].AsString())
out := make([]byte, len(in))
pos := len(out)
inB := []byte(in)
for i := 0; i < len(in); {
d, _, _ := textseg.ScanGraphemeClusters(inB[i:], true)
cluster := in[i : i+d]
pos -= len(cluster)
copy(out[pos:], cluster)
i += d
}
return cty.StringVal(string(out)), nil
},
})
var StrlenFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
in := args[0].AsString()
l := 0
inB := []byte(in)
for i := 0; i < len(in); {
d, _, _ := textseg.ScanGraphemeClusters(inB[i:], true)
l++
i += d
}
return cty.NumberIntVal(int64(l)), nil
},
})
var SubstrFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
AllowDynamicType: true,
},
{
Name: "offset",
Type: cty.Number,
AllowDynamicType: true,
},
{
Name: "length",
Type: cty.Number,
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
in := []byte(args[0].AsString())
var offset, length int
var err error
err = gocty.FromCtyValue(args[1], &offset)
if err != nil {
return cty.NilVal, err
}
err = gocty.FromCtyValue(args[2], &length)
if err != nil {
return cty.NilVal, err
}
if offset < 0 {
totalLenNum, err := Strlen(args[0])
if err != nil {
// should never happen
panic("Stdlen returned an error")
}
var totalLen int
err = gocty.FromCtyValue(totalLenNum, &totalLen)
if err != nil {
// should never happen
panic("Stdlen returned a non-int number")
}
offset += totalLen
} else if length == 0 {
// Short circuit here, after error checks, because if a
// string of length 0 has been requested it will always
// be the empty string
return cty.StringVal(""), nil
}
sub := in
pos := 0
var i int
// First we'll seek forward to our offset
if offset > 0 {
for i = 0; i < len(sub); {
d, _, _ := textseg.ScanGraphemeClusters(sub[i:], true)
i += d
pos++
if pos == offset {
break
}
if i >= len(in) {
return cty.StringVal(""), nil
}
}
sub = sub[i:]
}
if length < 0 {
// Taking the remainder of the string is a fast path since
// we can just return the rest of the buffer verbatim.
return cty.StringVal(string(sub)), nil
}
// Otherwise we need to start seeking forward again until we
// reach the length we want.
pos = 0
for i = 0; i < len(sub); {
d, _, _ := textseg.ScanGraphemeClusters(sub[i:], true)
i += d
pos++
if pos == length {
break
}
}
sub = sub[:i]
return cty.StringVal(string(sub)), nil
},
})
var JoinFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "separator",
Type: cty.String,
},
},
VarParam: &function.Parameter{
Name: "lists",
Type: cty.List(cty.String),
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
sep := args[0].AsString()
listVals := args[1:]
if len(listVals) < 1 {
return cty.UnknownVal(cty.String), fmt.Errorf("at least one list is required")
}
l := 0
for _, list := range listVals {
if !list.IsWhollyKnown() {
return cty.UnknownVal(cty.String), nil
}
l += list.LengthInt()
}
items := make([]string, 0, l)
for ai, list := range listVals {
ei := 0
for it := list.ElementIterator(); it.Next(); {
_, val := it.Element()
if val.IsNull() {
if len(listVals) > 1 {
return cty.UnknownVal(cty.String), function.NewArgErrorf(ai+1, "element %d of list %d is null; cannot concatenate null values", ei, ai+1)
}
return cty.UnknownVal(cty.String), function.NewArgErrorf(ai+1, "element %d is null; cannot concatenate null values", ei)
}
items = append(items, val.AsString())
ei++
}
}
return cty.StringVal(strings.Join(items, sep)), nil
},
})
var SortFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.List(cty.String),
},
},
Type: function.StaticReturnType(cty.List(cty.String)),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
listVal := args[0]
if !listVal.IsWhollyKnown() {
// If some of the element values aren't known yet then we
// can't yet predict the order of the result.
return cty.UnknownVal(retType), nil
}
if listVal.LengthInt() == 0 { // Easy path
return listVal, nil
}
list := make([]string, 0, listVal.LengthInt())
for it := listVal.ElementIterator(); it.Next(); {
iv, v := it.Element()
if v.IsNull() {
return cty.UnknownVal(retType), fmt.Errorf("given list element %s is null; a null string cannot be sorted", iv.AsBigFloat().String())
}
list = append(list, v.AsString())
}
sort.Strings(list)
retVals := make([]cty.Value, len(list))
for i, s := range list {
retVals[i] = cty.StringVal(s)
}
return cty.ListVal(retVals), nil
},
})
var SplitFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "separator",
Type: cty.String,
},
{
Name: "str",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.List(cty.String)),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
sep := args[0].AsString()
str := args[1].AsString()
elems := strings.Split(str, sep)
elemVals := make([]cty.Value, len(elems))
for i, s := range elems {
elemVals[i] = cty.StringVal(s)
}
if len(elemVals) == 0 {
return cty.ListValEmpty(cty.String), nil
}
return cty.ListVal(elemVals), nil
},
})
// ChompFunc is a function that removes newline characters at the end of a
// string.
var ChompFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
newlines := regexp.MustCompile(`(?:\r\n?|\n)*\z`)
return cty.StringVal(newlines.ReplaceAllString(args[0].AsString(), "")), nil
},
})
// IndentFunc is a function that adds a given number of spaces to the
// beginnings of all but the first line in a given multi-line string.
var IndentFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "spaces",
Type: cty.Number,
},
{
Name: "str",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
var spaces int
if err := gocty.FromCtyValue(args[0], &spaces); err != nil {
return cty.UnknownVal(cty.String), err
}
data := args[1].AsString()
pad := strings.Repeat(" ", spaces)
return cty.StringVal(strings.Replace(data, "\n", "\n"+pad, -1)), nil
},
})
// TitleFunc is a function that converts the first letter of each word in the
// given string to uppercase.
var TitleFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return cty.StringVal(strings.Title(args[0].AsString())), nil
},
})
// TrimSpaceFunc is a function that removes any space characters from the start
// and end of the given string.
var TrimSpaceFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return cty.StringVal(strings.TrimSpace(args[0].AsString())), nil
},
})
// TrimFunc is a function that removes the specified characters from the start
// and end of the given string.
var TrimFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
{
Name: "cutset",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
str := args[0].AsString()
cutset := args[1].AsString()
return cty.StringVal(strings.Trim(str, cutset)), nil
},
})
// TrimPrefixFunc is a function that removes the specified characters from the
// start the given string.
var TrimPrefixFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
{
Name: "prefix",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
str := args[0].AsString()
prefix := args[1].AsString()
return cty.StringVal(strings.TrimPrefix(str, prefix)), nil
},
})
// TrimSuffixFunc is a function that removes the specified characters from the
// end of the given string.
var TrimSuffixFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
{
Name: "suffix",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
str := args[0].AsString()
cutset := args[1].AsString()
return cty.StringVal(strings.TrimSuffix(str, cutset)), nil
},
})
// Upper is a Function that converts a given string to uppercase.
func Upper(str cty.Value) (cty.Value, error) {
return UpperFunc.Call([]cty.Value{str})
}
// Lower is a Function that converts a given string to lowercase.
func Lower(str cty.Value) (cty.Value, error) {
return LowerFunc.Call([]cty.Value{str})
}
// Reverse is a Function that reverses the order of the characters in the
// given string.
//
// As usual, "character" for the sake of this function is a grapheme cluster,
// so combining diacritics (for example) will be considered together as a
// single character.
func Reverse(str cty.Value) (cty.Value, error) {
return ReverseFunc.Call([]cty.Value{str})
}
// Strlen is a Function that returns the length of the given string in
// characters.
//
// As usual, "character" for the sake of this function is a grapheme cluster,
// so combining diacritics (for example) will be considered together as a
// single character.
func Strlen(str cty.Value) (cty.Value, error) {
return StrlenFunc.Call([]cty.Value{str})
}
// Substr is a Function that extracts a sequence of characters from another
// string and creates a new string.
//
// As usual, "character" for the sake of this function is a grapheme cluster,
// so combining diacritics (for example) will be considered together as a
// single character.
//
// The "offset" index may be negative, in which case it is relative to the
// end of the given string.
//
// The "length" may be -1, in which case the remainder of the string after
// the given offset will be returned.
func Substr(str cty.Value, offset cty.Value, length cty.Value) (cty.Value, error) {
return SubstrFunc.Call([]cty.Value{str, offset, length})
}
// Join concatenates together the string elements of one or more lists with a
// given separator.
func Join(sep cty.Value, lists ...cty.Value) (cty.Value, error) {
args := make([]cty.Value, len(lists)+1)
args[0] = sep
copy(args[1:], lists)
return JoinFunc.Call(args)
}
// Sort re-orders the elements of a given list of strings so that they are
// in ascending lexicographical order.
func Sort(list cty.Value) (cty.Value, error) {
return SortFunc.Call([]cty.Value{list})
}
// Split divides a given string by a given separator, returning a list of
// strings containing the characters between the separator sequences.
func Split(sep, str cty.Value) (cty.Value, error) {
return SplitFunc.Call([]cty.Value{sep, str})
}
// Chomp removes newline characters at the end of a string.
func Chomp(str cty.Value) (cty.Value, error) {
return ChompFunc.Call([]cty.Value{str})
}
// Indent adds a given number of spaces to the beginnings of all but the first
// line in a given multi-line string.
func Indent(spaces, str cty.Value) (cty.Value, error) {
return IndentFunc.Call([]cty.Value{spaces, str})
}
// Title converts the first letter of each word in the given string to uppercase.
func Title(str cty.Value) (cty.Value, error) {
return TitleFunc.Call([]cty.Value{str})
}
// TrimSpace removes any space characters from the start and end of the given string.
func TrimSpace(str cty.Value) (cty.Value, error) {
return TrimSpaceFunc.Call([]cty.Value{str})
}
// Trim removes the specified characters from the start and end of the given string.
func Trim(str, cutset cty.Value) (cty.Value, error) {
return TrimFunc.Call([]cty.Value{str, cutset})
}
// TrimPrefix removes the specified prefix from the start of the given string.
func TrimPrefix(str, prefix cty.Value) (cty.Value, error) {
return TrimPrefixFunc.Call([]cty.Value{str, prefix})
}
// TrimSuffix removes the specified suffix from the end of the given string.
func TrimSuffix(str, suffix cty.Value) (cty.Value, error) {
return TrimSuffixFunc.Call([]cty.Value{str, suffix})
}

View File

@ -0,0 +1,80 @@
package stdlib
import (
"regexp"
"strings"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
// ReplaceFunc is a function that searches a given string for another given
// substring, and replaces each occurence with a given replacement string.
// The substr argument is a simple string.
var ReplaceFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
{
Name: "substr",
Type: cty.String,
},
{
Name: "replace",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
str := args[0].AsString()
substr := args[1].AsString()
replace := args[2].AsString()
return cty.StringVal(strings.Replace(str, substr, replace, -1)), nil
},
})
// RegexReplaceFunc is a function that searches a given string for another
// given substring, and replaces each occurence with a given replacement
// string. The substr argument must be a valid regular expression.
var RegexReplaceFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
{
Name: "substr",
Type: cty.String,
},
{
Name: "replace",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
str := args[0].AsString()
substr := args[1].AsString()
replace := args[2].AsString()
re, err := regexp.Compile(substr)
if err != nil {
return cty.UnknownVal(cty.String), err
}
return cty.StringVal(re.ReplaceAllString(str, replace)), nil
},
})
// Replace searches a given string for another given substring,
// and replaces all occurrences with a given replacement string.
func Replace(str, substr, replace cty.Value) (cty.Value, error) {
return ReplaceFunc.Call([]cty.Value{str, substr, replace})
}
func RegexReplace(str, substr, replace cty.Value) (cty.Value, error) {
return RegexReplaceFunc.Call([]cty.Value{str, substr, replace})
}