mirror of https://github.com/databricks/cli.git
260 lines
6.3 KiB
Go
260 lines
6.3 KiB
Go
package yamlloader
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/databricks/cli/libs/dyn"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type loader struct {
|
|
path string
|
|
}
|
|
|
|
func errorf(loc dyn.Location, format string, args ...any) error {
|
|
return fmt.Errorf("yaml (%s): %s", loc, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
func newLoader(path string) *loader {
|
|
return &loader{
|
|
path: path,
|
|
}
|
|
}
|
|
|
|
func (d *loader) location(node *yaml.Node) dyn.Location {
|
|
return dyn.Location{
|
|
File: d.path,
|
|
Line: node.Line,
|
|
Column: node.Column,
|
|
}
|
|
}
|
|
|
|
func (d *loader) load(node *yaml.Node) (dyn.Value, error) {
|
|
loc := dyn.Location{
|
|
File: d.path,
|
|
Line: node.Line,
|
|
Column: node.Column,
|
|
}
|
|
|
|
var value dyn.Value
|
|
var err error
|
|
|
|
switch node.Kind {
|
|
case yaml.DocumentNode:
|
|
value, err = d.loadDocument(node, loc)
|
|
case yaml.SequenceNode:
|
|
value, err = d.loadSequence(node, loc)
|
|
case yaml.MappingNode:
|
|
value, err = d.loadMapping(node, loc)
|
|
case yaml.ScalarNode:
|
|
value, err = d.loadScalar(node, loc)
|
|
case yaml.AliasNode:
|
|
value, err = d.loadAlias(node, loc)
|
|
default:
|
|
return dyn.InvalidValue, errorf(loc, "unknown node kind: %v", node.Kind)
|
|
}
|
|
|
|
if err != nil {
|
|
return value, err
|
|
}
|
|
|
|
// Mark value as anchor if needed.
|
|
// If this node doesn't map to a type, we don't need to warn about it.
|
|
if node.Anchor != "" {
|
|
value = value.MarkAnchor()
|
|
}
|
|
|
|
return value, nil
|
|
}
|
|
|
|
func (d *loader) loadDocument(node *yaml.Node, loc dyn.Location) (dyn.Value, error) {
|
|
return d.load(node.Content[0])
|
|
}
|
|
|
|
func (d *loader) loadSequence(node *yaml.Node, loc dyn.Location) (dyn.Value, error) {
|
|
acc := make([]dyn.Value, len(node.Content))
|
|
for i, n := range node.Content {
|
|
v, err := d.load(n)
|
|
if err != nil {
|
|
return dyn.InvalidValue, err
|
|
}
|
|
|
|
acc[i] = v
|
|
}
|
|
|
|
return dyn.NewValue(acc, []dyn.Location{loc}), nil
|
|
}
|
|
|
|
func (d *loader) loadMapping(node *yaml.Node, loc dyn.Location) (dyn.Value, error) {
|
|
var merge *yaml.Node
|
|
|
|
acc := dyn.NewMapping()
|
|
for i := 0; i < len(node.Content); i += 2 {
|
|
key := node.Content[i]
|
|
val := node.Content[i+1]
|
|
|
|
// Assert that keys are strings
|
|
if key.Kind != yaml.ScalarNode {
|
|
return dyn.InvalidValue, errorf(loc, "key is not a scalar")
|
|
}
|
|
|
|
st := key.ShortTag()
|
|
switch st {
|
|
case "!!str":
|
|
// OK
|
|
case "!!null":
|
|
// A literal unquoted "null" is treated as a null value by the YAML parser.
|
|
// However, when used as a key, it is treated as the string "null".
|
|
case "!!merge":
|
|
if merge != nil {
|
|
panic("merge node already set")
|
|
}
|
|
merge = val
|
|
continue
|
|
default:
|
|
return dyn.InvalidValue, errorf(loc, "invalid key tag: %v", st)
|
|
}
|
|
|
|
k := dyn.NewValue(key.Value, []dyn.Location{{
|
|
File: d.path,
|
|
Line: key.Line,
|
|
Column: key.Column,
|
|
}})
|
|
|
|
v, err := d.load(val)
|
|
if err != nil {
|
|
return dyn.InvalidValue, err
|
|
}
|
|
|
|
acc.Set(k, v)
|
|
}
|
|
|
|
if merge == nil {
|
|
return dyn.NewValue(acc, []dyn.Location{loc}), nil
|
|
}
|
|
|
|
// Build location for the merge node.
|
|
var mloc = d.location(merge)
|
|
var merr = errorf(mloc, "map merge requires map or sequence of maps as the value")
|
|
|
|
// Flatten the merge node into a slice of nodes.
|
|
// It can be either a single node or a sequence of nodes.
|
|
var mnodes []*yaml.Node
|
|
switch merge.Kind {
|
|
case yaml.SequenceNode:
|
|
mnodes = merge.Content
|
|
case yaml.AliasNode:
|
|
mnodes = []*yaml.Node{merge}
|
|
default:
|
|
return dyn.NilValue, merr
|
|
}
|
|
|
|
// Build a sequence of values to merge.
|
|
// The entries that we already accumulated have precedence.
|
|
var seq []dyn.Mapping
|
|
for _, n := range mnodes {
|
|
v, err := d.load(n)
|
|
if err != nil {
|
|
return dyn.InvalidValue, err
|
|
}
|
|
m, ok := v.AsMap()
|
|
if !ok {
|
|
return dyn.NilValue, merr
|
|
}
|
|
seq = append(seq, m)
|
|
}
|
|
|
|
// Append the accumulated entries to the sequence.
|
|
seq = append(seq, acc)
|
|
out := dyn.NewMapping()
|
|
for _, m := range seq {
|
|
out.Merge(m)
|
|
}
|
|
|
|
return dyn.NewValue(out, []dyn.Location{loc}), nil
|
|
}
|
|
|
|
func newIntValue(i64 int64, loc dyn.Location) dyn.Value {
|
|
// Use regular int type instead of int64 if possible.
|
|
if i64 >= math.MinInt32 && i64 <= math.MaxInt32 {
|
|
return dyn.NewValue(int(i64), []dyn.Location{loc})
|
|
}
|
|
return dyn.NewValue(i64, []dyn.Location{loc})
|
|
}
|
|
|
|
func (d *loader) loadScalar(node *yaml.Node, loc dyn.Location) (dyn.Value, error) {
|
|
st := node.ShortTag()
|
|
switch st {
|
|
case "!!str":
|
|
return dyn.NewValue(node.Value, []dyn.Location{loc}), nil
|
|
case "!!bool":
|
|
switch strings.ToLower(node.Value) {
|
|
case "true":
|
|
return dyn.NewValue(true, []dyn.Location{loc}), nil
|
|
case "false":
|
|
return dyn.NewValue(false, []dyn.Location{loc}), nil
|
|
default:
|
|
return dyn.InvalidValue, errorf(loc, "invalid bool value: %v", node.Value)
|
|
}
|
|
case "!!int":
|
|
// Try to parse the an integer value in base 10.
|
|
// We trim leading zeros to avoid octal parsing of the "0" prefix.
|
|
// See "testdata/spec_example_2.19.yml" for background.
|
|
i64, err := strconv.ParseInt(strings.TrimLeft(node.Value, "0"), 10, 64)
|
|
if err == nil {
|
|
return newIntValue(i64, loc), nil
|
|
}
|
|
// Let the [ParseInt] function figure out the base.
|
|
i64, err = strconv.ParseInt(node.Value, 0, 64)
|
|
if err == nil {
|
|
return newIntValue(i64, loc), nil
|
|
}
|
|
return dyn.InvalidValue, errorf(loc, "invalid int value: %v", node.Value)
|
|
case "!!float":
|
|
f64, err := strconv.ParseFloat(node.Value, 64)
|
|
if err != nil {
|
|
// Deal with infinity prefixes.
|
|
v := strings.ToLower(node.Value)
|
|
switch {
|
|
case strings.HasPrefix(v, "+"):
|
|
v = strings.TrimPrefix(v, "+")
|
|
f64 = math.Inf(1)
|
|
case strings.HasPrefix(v, "-"):
|
|
v = strings.TrimPrefix(v, "-")
|
|
f64 = math.Inf(-1)
|
|
default:
|
|
// No prefix.
|
|
f64 = math.Inf(1)
|
|
}
|
|
|
|
// Deal with infinity and NaN values.
|
|
switch v {
|
|
case ".inf":
|
|
return dyn.NewValue(f64, []dyn.Location{loc}), nil
|
|
case ".nan":
|
|
return dyn.NewValue(math.NaN(), []dyn.Location{loc}), nil
|
|
}
|
|
|
|
return dyn.InvalidValue, errorf(loc, "invalid float value: %v", node.Value)
|
|
}
|
|
return dyn.NewValue(f64, []dyn.Location{loc}), nil
|
|
case "!!null":
|
|
return dyn.NewValue(nil, []dyn.Location{loc}), nil
|
|
case "!!timestamp":
|
|
t, err := dyn.NewTime(node.Value)
|
|
if err == nil {
|
|
return dyn.NewValue(t, []dyn.Location{loc}), nil
|
|
}
|
|
return dyn.InvalidValue, errorf(loc, "invalid timestamp value: %v", node.Value)
|
|
default:
|
|
return dyn.InvalidValue, errorf(loc, "unknown tag: %v", st)
|
|
}
|
|
}
|
|
|
|
func (d *loader) loadAlias(node *yaml.Node, loc dyn.Location) (dyn.Value, error) {
|
|
return d.load(node.Alias)
|
|
}
|