mirror of
https://github.com/mikefarah/yq.git
synced 2026-03-10 15:54:26 +00:00
453 lines
12 KiB
Go
453 lines
12 KiB
Go
//go:build !yq_notoml
|
|
|
|
package yqlib
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
toml "github.com/pelletier/go-toml/v2/unstable"
|
|
)
|
|
|
|
type tomlDecoder struct {
|
|
parser toml.Parser
|
|
finished bool
|
|
d DataTreeNavigator
|
|
rootMap *CandidateNode
|
|
pendingComments []string // Head comments collected from Comment nodes
|
|
firstContentSeen bool // Track if we've processed the first non-comment node
|
|
}
|
|
|
|
func NewTomlDecoder() Decoder {
|
|
return &tomlDecoder{
|
|
finished: false,
|
|
d: NewDataTreeNavigator(),
|
|
}
|
|
}
|
|
|
|
func (dec *tomlDecoder) Init(reader io.Reader) error {
|
|
dec.parser = toml.Parser{KeepComments: true}
|
|
buf := new(bytes.Buffer)
|
|
_, err := buf.ReadFrom(reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dec.parser.Reset(buf.Bytes())
|
|
dec.rootMap = &CandidateNode{
|
|
Kind: MappingNode,
|
|
Tag: "!!map",
|
|
}
|
|
dec.pendingComments = make([]string, 0)
|
|
dec.firstContentSeen = false
|
|
return nil
|
|
}
|
|
|
|
func (dec *tomlDecoder) getFullPath(tomlNode *toml.Node) []interface{} {
|
|
path := make([]interface{}, 0)
|
|
for {
|
|
path = append(path, string(tomlNode.Data))
|
|
tomlNode = tomlNode.Next()
|
|
if tomlNode == nil {
|
|
return path
|
|
}
|
|
}
|
|
}
|
|
|
|
func (dec *tomlDecoder) processKeyValueIntoMap(rootMap *CandidateNode, tomlNode *toml.Node) error {
|
|
value := tomlNode.Value()
|
|
path := dec.getFullPath(value.Next())
|
|
|
|
valueNode, err := dec.decodeNode(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Attach pending head comments
|
|
if len(dec.pendingComments) > 0 {
|
|
valueNode.HeadComment = strings.Join(dec.pendingComments, "\n")
|
|
dec.pendingComments = make([]string, 0)
|
|
}
|
|
|
|
// Check for inline comment chained to the KeyValue node
|
|
nextNode := tomlNode.Next()
|
|
if nextNode != nil && nextNode.Kind == toml.Comment {
|
|
valueNode.LineComment = string(nextNode.Data)
|
|
}
|
|
|
|
context := Context{}
|
|
context = context.SingleChildContext(rootMap)
|
|
|
|
return dec.d.DeeplyAssign(context, path, valueNode)
|
|
}
|
|
|
|
func (dec *tomlDecoder) decodeKeyValuesIntoMap(rootMap *CandidateNode, tomlNode *toml.Node) (bool, error) {
|
|
log.Debug("decodeKeyValuesIntoMap -- processing first (current) entry")
|
|
if err := dec.processKeyValueIntoMap(rootMap, tomlNode); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
for dec.parser.NextExpression() {
|
|
nextItem := dec.parser.Expression()
|
|
log.Debug("decodeKeyValuesIntoMap -- next exp, its a %v", nextItem.Kind)
|
|
|
|
switch nextItem.Kind {
|
|
case toml.KeyValue:
|
|
if err := dec.processKeyValueIntoMap(rootMap, nextItem); err != nil {
|
|
return false, err
|
|
}
|
|
case toml.Comment:
|
|
// Standalone comment - add to pending for next element
|
|
dec.pendingComments = append(dec.pendingComments, string(nextItem.Data))
|
|
default:
|
|
// run out of key values
|
|
log.Debug("done in decodeKeyValuesIntoMap, gota a %v", nextItem.Kind)
|
|
return true, nil
|
|
}
|
|
}
|
|
log.Debug("no more things to read in")
|
|
return false, nil
|
|
}
|
|
|
|
func (dec *tomlDecoder) createInlineTableMap(tomlNode *toml.Node) (*CandidateNode, error) {
|
|
content := make([]*CandidateNode, 0)
|
|
log.Debug("createInlineTableMap")
|
|
|
|
iterator := tomlNode.Children()
|
|
for iterator.Next() {
|
|
child := iterator.Node()
|
|
if child.Kind != toml.KeyValue {
|
|
return nil, fmt.Errorf("only keyvalue pairs are supported in inlinetables, got %v instead", child.Kind)
|
|
}
|
|
|
|
keyValues := &CandidateNode{
|
|
Kind: MappingNode,
|
|
Tag: "!!map",
|
|
}
|
|
|
|
if err := dec.processKeyValueIntoMap(keyValues, child); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
content = append(content, keyValues.Content...)
|
|
}
|
|
|
|
return &CandidateNode{
|
|
Kind: MappingNode,
|
|
Tag: "!!map",
|
|
Content: content,
|
|
}, nil
|
|
}
|
|
|
|
func (dec *tomlDecoder) createArray(tomlNode *toml.Node) (*CandidateNode, error) {
|
|
content := make([]*CandidateNode, 0)
|
|
iterator := tomlNode.Children()
|
|
for iterator.Next() {
|
|
child := iterator.Node()
|
|
yamlNode, err := dec.decodeNode(child)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
content = append(content, yamlNode)
|
|
}
|
|
|
|
return &CandidateNode{
|
|
Kind: SequenceNode,
|
|
Tag: "!!seq",
|
|
Content: content,
|
|
}, nil
|
|
|
|
}
|
|
|
|
func (dec *tomlDecoder) createStringScalar(tomlNode *toml.Node) (*CandidateNode, error) {
|
|
content := string(tomlNode.Data)
|
|
return createScalarNode(content, content), nil
|
|
}
|
|
|
|
func (dec *tomlDecoder) createBoolScalar(tomlNode *toml.Node) (*CandidateNode, error) {
|
|
content := string(tomlNode.Data)
|
|
return createScalarNode(content == "true", content), nil
|
|
}
|
|
|
|
func (dec *tomlDecoder) createIntegerScalar(tomlNode *toml.Node) (*CandidateNode, error) {
|
|
content := string(tomlNode.Data)
|
|
_, num, err := parseInt64(content)
|
|
return createScalarNode(num, content), err
|
|
}
|
|
|
|
func (dec *tomlDecoder) createDateTimeScalar(tomlNode *toml.Node) (*CandidateNode, error) {
|
|
content := string(tomlNode.Data)
|
|
val, err := parseDateTime(time.RFC3339, content)
|
|
return createScalarNode(val, content), err
|
|
}
|
|
|
|
func (dec *tomlDecoder) createFloatScalar(tomlNode *toml.Node) (*CandidateNode, error) {
|
|
content := string(tomlNode.Data)
|
|
num, err := strconv.ParseFloat(content, 64)
|
|
return createScalarNode(num, content), err
|
|
}
|
|
|
|
func (dec *tomlDecoder) decodeNode(tomlNode *toml.Node) (*CandidateNode, error) {
|
|
switch tomlNode.Kind {
|
|
case toml.Key, toml.String:
|
|
return dec.createStringScalar(tomlNode)
|
|
case toml.Bool:
|
|
return dec.createBoolScalar(tomlNode)
|
|
case toml.Integer:
|
|
return dec.createIntegerScalar(tomlNode)
|
|
case toml.DateTime:
|
|
return dec.createDateTimeScalar(tomlNode)
|
|
case toml.Float:
|
|
return dec.createFloatScalar(tomlNode)
|
|
case toml.Array:
|
|
return dec.createArray(tomlNode)
|
|
case toml.InlineTable:
|
|
return dec.createInlineTableMap(tomlNode)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported type %v", tomlNode.Kind)
|
|
}
|
|
|
|
}
|
|
|
|
func (dec *tomlDecoder) Decode() (*CandidateNode, error) {
|
|
if dec.finished {
|
|
return nil, io.EOF
|
|
}
|
|
//
|
|
// toml library likes to panic
|
|
var deferredError error
|
|
defer func() { //catch or finally
|
|
if r := recover(); r != nil {
|
|
var ok bool
|
|
deferredError, ok = r.(error)
|
|
if !ok {
|
|
deferredError = fmt.Errorf("pkg: %v", r)
|
|
}
|
|
}
|
|
}()
|
|
|
|
log.Debug("ok here we go")
|
|
var runAgainstCurrentExp = false
|
|
var err error
|
|
for runAgainstCurrentExp || dec.parser.NextExpression() {
|
|
|
|
if runAgainstCurrentExp {
|
|
log.Debug("running against current exp")
|
|
}
|
|
|
|
currentNode := dec.parser.Expression()
|
|
|
|
log.Debug("currentNode: %v ", currentNode.Kind)
|
|
runAgainstCurrentExp, err = dec.processTopLevelNode(currentNode)
|
|
if err != nil {
|
|
return dec.rootMap, err
|
|
}
|
|
|
|
}
|
|
|
|
err = dec.parser.Error()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// must have finished
|
|
dec.finished = true
|
|
|
|
if len(dec.rootMap.Content) == 0 {
|
|
return nil, io.EOF
|
|
}
|
|
|
|
return dec.rootMap, deferredError
|
|
|
|
}
|
|
|
|
func (dec *tomlDecoder) processTopLevelNode(currentNode *toml.Node) (bool, error) {
|
|
var runAgainstCurrentExp bool
|
|
var err error
|
|
log.Debug("processTopLevelNode: Going to process %v state is current %v", currentNode.Kind, NodeToString(dec.rootMap))
|
|
switch currentNode.Kind {
|
|
case toml.Comment:
|
|
// Collect comment to attach to next element
|
|
commentText := string(currentNode.Data)
|
|
// If we haven't seen any content yet, accumulate comments for root
|
|
if !dec.firstContentSeen {
|
|
if dec.rootMap.HeadComment == "" {
|
|
dec.rootMap.HeadComment = commentText
|
|
} else {
|
|
dec.rootMap.HeadComment = dec.rootMap.HeadComment + "\n" + commentText
|
|
}
|
|
} else {
|
|
// We've seen content, so these comments are for the next element
|
|
dec.pendingComments = append(dec.pendingComments, commentText)
|
|
}
|
|
return false, nil
|
|
case toml.Table:
|
|
dec.firstContentSeen = true
|
|
runAgainstCurrentExp, err = dec.processTable(currentNode)
|
|
case toml.ArrayTable:
|
|
dec.firstContentSeen = true
|
|
runAgainstCurrentExp, err = dec.processArrayTable(currentNode)
|
|
default:
|
|
dec.firstContentSeen = true
|
|
runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(dec.rootMap, currentNode)
|
|
}
|
|
|
|
log.Debug("processTopLevelNode: DONE Processing state is now %v", NodeToString(dec.rootMap))
|
|
return runAgainstCurrentExp, err
|
|
}
|
|
|
|
func (dec *tomlDecoder) processTable(currentNode *toml.Node) (bool, error) {
|
|
log.Debug("Enter processTable")
|
|
child := currentNode.Child()
|
|
fullPath := dec.getFullPath(child)
|
|
log.Debug("fullpath: %v", fullPath)
|
|
|
|
c := Context{}
|
|
c = c.SingleChildContext(dec.rootMap)
|
|
|
|
fullPath, err := getPathToUse(fullPath, dec, c)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
tableNodeValue := &CandidateNode{
|
|
Kind: MappingNode,
|
|
Tag: "!!map",
|
|
Content: make([]*CandidateNode, 0),
|
|
EncodeSeparate: true,
|
|
}
|
|
|
|
// Attach pending head comments to the table
|
|
if len(dec.pendingComments) > 0 {
|
|
tableNodeValue.HeadComment = strings.Join(dec.pendingComments, "\n")
|
|
dec.pendingComments = make([]string, 0)
|
|
}
|
|
|
|
var tableValue *toml.Node
|
|
runAgainstCurrentExp := false
|
|
hasValue := dec.parser.NextExpression()
|
|
// check to see if there is any table data
|
|
if hasValue {
|
|
tableValue = dec.parser.Expression()
|
|
// next expression is not table data, so we are done
|
|
if tableValue.Kind != toml.KeyValue {
|
|
log.Debug("got an empty table")
|
|
runAgainstCurrentExp = true
|
|
} else {
|
|
runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(tableNodeValue, tableValue)
|
|
if err != nil && !errors.Is(err, io.EOF) {
|
|
return false, err
|
|
}
|
|
}
|
|
}
|
|
|
|
err = dec.d.DeeplyAssign(c, fullPath, tableNodeValue)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return runAgainstCurrentExp, nil
|
|
}
|
|
|
|
func (dec *tomlDecoder) arrayAppend(context Context, path []interface{}, rhsNode *CandidateNode) error {
|
|
log.Debug("arrayAppend to path: %v,%v", path, NodeToString(rhsNode))
|
|
rhsCandidateNode := &CandidateNode{
|
|
Kind: SequenceNode,
|
|
Tag: "!!seq",
|
|
Content: []*CandidateNode{rhsNode},
|
|
}
|
|
|
|
assignmentOp := &Operation{OperationType: addAssignOpType}
|
|
|
|
rhsOp := &Operation{OperationType: valueOpType, CandidateNode: rhsCandidateNode}
|
|
|
|
assignmentOpNode := &ExpressionNode{
|
|
Operation: assignmentOp,
|
|
LHS: createTraversalTree(path, traversePreferences{}, false),
|
|
RHS: &ExpressionNode{Operation: rhsOp},
|
|
}
|
|
|
|
_, err := dec.d.GetMatchingNodes(context, assignmentOpNode)
|
|
return err
|
|
}
|
|
|
|
func (dec *tomlDecoder) processArrayTable(currentNode *toml.Node) (bool, error) {
|
|
log.Debug("Enter processArrayTable")
|
|
child := currentNode.Child()
|
|
fullPath := dec.getFullPath(child)
|
|
log.Debug("Fullpath: %v", fullPath)
|
|
|
|
c := Context{}
|
|
c = c.SingleChildContext(dec.rootMap)
|
|
|
|
fullPath, err := getPathToUse(fullPath, dec, c)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// need to use the array append exp to add another entry to
|
|
// this array: fullpath += [ thing ]
|
|
hasValue := dec.parser.NextExpression()
|
|
|
|
tableNodeValue := &CandidateNode{
|
|
Kind: MappingNode,
|
|
Tag: "!!map",
|
|
EncodeSeparate: true,
|
|
}
|
|
|
|
// Attach pending head comments to the array table
|
|
if len(dec.pendingComments) > 0 {
|
|
tableNodeValue.HeadComment = strings.Join(dec.pendingComments, "\n")
|
|
dec.pendingComments = make([]string, 0)
|
|
}
|
|
|
|
runAgainstCurrentExp := false
|
|
// if the next value is a ArrayTable or Table, then its not part of this declaration (not a key value pair)
|
|
// so lets leave that expression for the next round of parsing
|
|
if hasValue && (dec.parser.Expression().Kind == toml.ArrayTable || dec.parser.Expression().Kind == toml.Table) {
|
|
runAgainstCurrentExp = true
|
|
} else if hasValue {
|
|
// otherwise, if there is a value, it must be some key value pairs of the
|
|
// first object in the array!
|
|
tableValue := dec.parser.Expression()
|
|
runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(tableNodeValue, tableValue)
|
|
if err != nil && !errors.Is(err, io.EOF) {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
// += function
|
|
err = dec.arrayAppend(c, fullPath, tableNodeValue)
|
|
|
|
return runAgainstCurrentExp, err
|
|
}
|
|
|
|
// if fullPath points to an array of maps rather than a map
|
|
// then it should set this element into the _last_ element of that array.
|
|
// Because TOML. So we'll inject the last index into the path.
|
|
|
|
func getPathToUse(fullPath []interface{}, dec *tomlDecoder, c Context) ([]interface{}, error) {
|
|
pathToCheck := fullPath
|
|
if len(fullPath) >= 1 {
|
|
pathToCheck = fullPath[:len(fullPath)-1]
|
|
}
|
|
readOp := createTraversalTree(pathToCheck, traversePreferences{DontAutoCreate: true}, false)
|
|
|
|
resultContext, err := dec.d.GetMatchingNodes(c, readOp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resultContext.MatchingNodes.Len() >= 1 {
|
|
match := resultContext.MatchingNodes.Front().Value.(*CandidateNode)
|
|
// path refers to an array, we need to add this to the last element in the array
|
|
if match.Kind == SequenceNode {
|
|
fullPath = append(pathToCheck, len(match.Content)-1, fullPath[len(fullPath)-1])
|
|
log.Debugf("Adding to end of %v array, using path: %v", pathToCheck, fullPath)
|
|
}
|
|
}
|
|
return fullPath, err
|
|
}
|