package yqlib import ( "fmt" "strings" yaml "github.com/goccy/go-yaml" "github.com/goccy/go-yaml/ast" goccyToken "github.com/goccy/go-yaml/token" ) func (o *CandidateNode) goccyDecodeIntoChild(childNode ast.Node, cm yaml.CommentMap, anchorMap map[string]*CandidateNode) (*CandidateNode, error) { newChild := o.CreateChild() err := newChild.UnmarshalGoccyYAML(childNode, cm, anchorMap) return newChild, err } func (o *CandidateNode) UnmarshalGoccyYAML(node ast.Node, cm yaml.CommentMap, anchorMap map[string]*CandidateNode) error { if node.GetComment() != nil { commentMapComments := cm[node.GetPath()] for _, comment := range node.GetComment().Comments { for _, commentMapComment := range commentMapComments { commentMapValue := strings.Join(commentMapComment.Texts, "\n") if commentMapValue == comment.Token.Value { switch commentMapComment.Position { case yaml.CommentHeadPosition: o.HeadComment = comment.String() case yaml.CommentLinePosition: o.LineComment = comment.String() case yaml.CommentFootPosition: o.FootComment = comment.String() } } } } } o.Value = node.String() o.Line = node.GetToken().Position.Line o.Column = node.GetToken().Position.Column switch node.Type() { case ast.IntegerType: o.Kind = ScalarNode o.Tag = "!!int" case ast.FloatType: o.Kind = ScalarNode o.Tag = "!!float" case ast.BoolType: o.Kind = ScalarNode o.Tag = "!!bool" case ast.NullType: o.Kind = ScalarNode o.Tag = "!!null" o.Value = node.GetToken().Value case ast.StringType: o.Kind = ScalarNode o.Tag = "!!str" switch node.GetToken().Type { case goccyToken.SingleQuoteType: o.Style = SingleQuotedStyle case goccyToken.DoubleQuoteType: o.Style = DoubleQuotedStyle } o.Value = node.(*ast.StringNode).Value case ast.LiteralType: o.Kind = ScalarNode o.Tag = "!!str" o.Style = LiteralStyle astLiteral := node.(*ast.LiteralNode) if astLiteral.Start.Type == goccyToken.FoldedType { o.Style = FoldedStyle } // Preserving the original multiline string value is important for fidelity. // goccy/go-yaml provides this in astLiteral.Value.Value for literal and folded styles. o.Value = astLiteral.Value.Value case ast.TagType: // Recursively unmarshal the tagged value, then apply the tag to the CandidateNode. if err := o.UnmarshalGoccyYAML(node.(*ast.TagNode).Value, cm, anchorMap); err != nil { return err } o.Tag = node.(*ast.TagNode).Start.Value // Tag value includes the '!' or '!!' prefix. case ast.MappingType: o.Kind = MappingNode o.Tag = "!!map" mappingNode := node.(*ast.MappingNode) if mappingNode.IsFlowStyle { o.Style = FlowStyle } for _, mappingValueNode := range mappingNode.Values { err := o.goccyProcessMappingValueNode(mappingValueNode, cm, anchorMap) if err != nil { return err } } if mappingNode.FootComment != nil { o.FootComment = mappingNode.FootComment.String() } case ast.MappingValueType: o.Kind = MappingNode o.Tag = "!!map" mappingValueNode := node.(*ast.MappingValueNode) err := o.goccyProcessMappingValueNode(mappingValueNode, cm, anchorMap) if err != nil { return err } case ast.SequenceType: o.Kind = SequenceNode o.Tag = "!!seq" sequenceNode := node.(*ast.SequenceNode) if sequenceNode.IsFlowStyle { o.Style = FlowStyle } astSeq := sequenceNode.Values o.Content = make([]*CandidateNode, len(astSeq)) for i := 0; i < len(astSeq); i++ { keyNode := o.CreateChild() keyNode.IsMapKey = true keyNode.Tag = "!!int" keyNode.Kind = ScalarNode keyNode.Value = fmt.Sprintf("%v", i) valueNode, err := o.goccyDecodeIntoChild(astSeq[i], cm, anchorMap) if err != nil { return err } valueNode.Key = keyNode o.Content[i] = valueNode } case ast.AnchorType: anchorNode := node.(*ast.AnchorNode) err := o.UnmarshalGoccyYAML(anchorNode.Value, cm, anchorMap) if err != nil { return err } o.Anchor = anchorNode.Name.String() anchorMap[o.Anchor] = o case ast.AliasType: aliasNode := node.(*ast.AliasNode) o.Kind = AliasNode o.Value = aliasNode.Value.String() o.Alias = anchorMap[o.Value] case ast.MergeKeyType: o.Kind = ScalarNode o.Tag = "!!merge" o.Value = "<<" default: log.Debugf("UnmarshalYAML - no idea of the type!!\n%v: %v", node.Type(), node.String()) } log.Debugf("KIND: %v", o.Kind) return nil } func (o *CandidateNode) goccyProcessMappingValueNode(mappingEntry *ast.MappingValueNode, cm yaml.CommentMap, anchorMap map[string]*CandidateNode) error { log.Debug("UnmarshalYAML MAP KEY entry %v", mappingEntry.Key) // AddKeyValueFirst because it clones the nodes, and we want to have the real refs when Unmarshalling // particularly for the anchorMap keyNode, valueNode := o.AddKeyValueChild(&CandidateNode{}, &CandidateNode{}) if err := keyNode.UnmarshalGoccyYAML(mappingEntry.Key, cm, anchorMap); err != nil { return err } log.Debug("UnmarshalYAML MAP VALUE entry %v", mappingEntry.Value) if err := valueNode.UnmarshalGoccyYAML(mappingEntry.Value, cm, anchorMap); err != nil { return err } if mappingEntry.FootComment != nil { valueNode.FootComment = mappingEntry.FootComment.String() } return nil } func (o *CandidateNode) MarshalGoccyYAML() (interface{}, error) { log.Debug("MarshalGoccyYAML to goccy: %v", o.Tag) switch o.Kind { case AliasNode: log.Debug("MarshalGoccyYAML - alias to goccy: %v", o.Tag) // For goccy, we'll return the referenced value directly // The goccy encoder will handle alias creation if o.Alias != nil { return o.Alias.MarshalGoccyYAML() } return o.Value, nil case ScalarNode: // Handle different scalar types based on tag for correct marshalling. switch o.Tag { case "!!int": val, err := parseInt(o.Value) if err == nil { return val, nil } return nil, fmt.Errorf("cannot marshal node %s as int: %w", NodeToString(o), err) case "!!float": val, err := parseFloat(o.Value) if err == nil { return val, nil } return nil, fmt.Errorf("cannot marshal node %s as float: %w", NodeToString(o), err) case "!!bool": val, err := parseBool(o.Value) if err == nil { return val, nil } return nil, fmt.Errorf("cannot marshal node %s as bool: %w", NodeToString(o), err) case "!!null": // goccy/go-yaml expects a nil interface{} for null values. return nil, nil default: // For standard strings (!!str) or unknown/custom tags, marshal as a string. // The goccy encoder will handle quoting and style if it's a plain string. // For custom tags, goccy prepends the tag if the value is a string. return o.Value, nil } case MappingNode: log.Debug("MarshalGoccyYAML - mapping: %v", NodeToString(o)) // Ensure even number of children for key-value pairs if len(o.Content)%2 != 0 { return nil, fmt.Errorf("mapping node at %s has an odd number of children (%d), malformed key-value pairs", NodeToString(o), len(o.Content)) } result := make(map[string]interface{}) for i := 0; i < len(o.Content); i += 2 { // No need to check i+1 >= len(o.Content) here due to the check above keyNode := o.Content[i] valueNode := o.Content[i+1] key := keyNode.Value if key == "" { key = NodeToString(keyNode) } value, err := valueNode.MarshalGoccyYAML() if err != nil { return nil, err } result[key] = value } return result, nil case SequenceNode: log.Debug("MarshalGoccyYAML - sequence: %v", NodeToString(o)) result := make([]interface{}, len(o.Content)) for i, childNode := range o.Content { value, err := childNode.MarshalGoccyYAML() if err != nil { return nil, err } result[i] = value } return result, nil } // Default case return o.Value, nil }