mirror of
https://github.com/mikefarah/yq.git
synced 2026-07-08 06:45:38 +00:00
Transition tests to skip variables, add Goccy specific tests in their place, add datetime preprocessor for date time difference
This commit is contained in:
parent
67572e3027
commit
c956c31b90
191
pkg/yqlib/datetime_preprocessor.go
Normal file
191
pkg/yqlib/datetime_preprocessor.go
Normal file
@ -0,0 +1,191 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DateTimePreprocessor handles automatic timestamp tagging for Goccy parser compatibility
|
||||
type DateTimePreprocessor struct {
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// NewDateTimePreprocessor creates a new datetime preprocessor
|
||||
func NewDateTimePreprocessor(enabled bool) *DateTimePreprocessor {
|
||||
return &DateTimePreprocessor{enabled: enabled}
|
||||
}
|
||||
|
||||
// ISO8601 date/time patterns that should be automatically tagged as timestamps
|
||||
var dateTimePatterns = []*regexp.Regexp{
|
||||
// RFC3339 / ISO8601 with timezone: 2006-01-02T15:04:05Z or 2006-01-02T15:04:05+07:00
|
||||
regexp.MustCompile(`^\s*([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?(?:Z|[+-][0-9]{2}:[0-9]{2}))\s*$`),
|
||||
// Date only: 2006-01-02
|
||||
regexp.MustCompile(`^\s*([0-9]{4}-[0-9]{2}-[0-9]{2})\s*$`),
|
||||
// RFC3339 without timezone: 2006-01-02T15:04:05
|
||||
regexp.MustCompile(`^\s*([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?)\s*$`),
|
||||
}
|
||||
|
||||
// isValidDateTime checks if a string represents a valid ISO8601/RFC3339 datetime
|
||||
func isValidDateTime(value string) bool {
|
||||
// Try to parse with RFC3339 first
|
||||
if _, err := time.Parse(time.RFC3339, value); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Try to parse date-only format
|
||||
if _, err := time.Parse("2006-01-02", value); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Try RFC3339 without timezone
|
||||
if _, err := time.Parse("2006-01-02T15:04:05", value); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// PreprocessDocument automatically adds !!timestamp tags to ISO8601 datetime strings
|
||||
// in YAML documents when using Goccy parser for consistent datetime arithmetic behaviour
|
||||
func (dtp *DateTimePreprocessor) PreprocessDocument(yamlContent string) string {
|
||||
if !dtp.enabled {
|
||||
return yamlContent
|
||||
}
|
||||
|
||||
lines := strings.Split(yamlContent, "\n")
|
||||
var result strings.Builder
|
||||
|
||||
for i, line := range lines {
|
||||
processed := dtp.processLine(line)
|
||||
result.WriteString(processed)
|
||||
|
||||
// Add newline except for the last line (preserve original ending)
|
||||
if i < len(lines)-1 {
|
||||
result.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// processLine processes a single line of YAML, adding timestamp tags where appropriate
|
||||
func (dtp *DateTimePreprocessor) processLine(line string) string {
|
||||
// Skip lines that are comments, already have tags, or are part of multi-line constructs
|
||||
if isSkippableLine(line) {
|
||||
return line
|
||||
}
|
||||
|
||||
// Look for key-value pairs: "key: value" or "- value"
|
||||
trimLeft := strings.TrimLeft(line, " \t")
|
||||
if strings.HasPrefix(trimLeft, "- ") {
|
||||
// Handle array items first (before key-value pairs)
|
||||
return dtp.processArrayItemLine(line)
|
||||
} else if colonIndex := strings.Index(line, ":"); colonIndex != -1 {
|
||||
// Handle map key-value pairs
|
||||
return dtp.processKeyValueLine(line, colonIndex)
|
||||
}
|
||||
|
||||
return line
|
||||
}
|
||||
|
||||
// isSkippableLine checks if a line should be skipped from datetime preprocessing
|
||||
func isSkippableLine(line string) bool {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
// Skip empty lines, comments, directives, document separators
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") ||
|
||||
strings.HasPrefix(trimmed, "%") || strings.HasPrefix(trimmed, "---") ||
|
||||
strings.HasPrefix(trimmed, "...") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Skip lines that already have explicit tags
|
||||
if strings.Contains(line, "!!") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Skip multi-line scalar indicators
|
||||
if strings.HasSuffix(trimmed, "|") || strings.HasSuffix(trimmed, ">") ||
|
||||
strings.HasSuffix(trimmed, "|-") || strings.HasSuffix(trimmed, ">-") ||
|
||||
strings.HasSuffix(trimmed, "|+") || strings.HasSuffix(trimmed, ">+") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// processKeyValueLine processes a line containing a key-value pair
|
||||
func (dtp *DateTimePreprocessor) processKeyValueLine(line string, colonIndex int) string {
|
||||
beforeColon := line[:colonIndex]
|
||||
afterColon := line[colonIndex+1:]
|
||||
|
||||
// Check if the value part looks like a datetime
|
||||
trimmedValue := strings.TrimSpace(afterColon)
|
||||
|
||||
// Skip if value is empty, quoted, or already complex
|
||||
if trimmedValue == "" ||
|
||||
strings.HasPrefix(trimmedValue, "\"") ||
|
||||
strings.HasPrefix(trimmedValue, "'") ||
|
||||
strings.HasPrefix(trimmedValue, "{") ||
|
||||
strings.HasPrefix(trimmedValue, "[") ||
|
||||
strings.HasPrefix(trimmedValue, "&") ||
|
||||
strings.HasPrefix(trimmedValue, "*") {
|
||||
return line
|
||||
}
|
||||
|
||||
// Check if it matches datetime patterns and is valid
|
||||
if dtp.matchesDateTimePattern(trimmedValue) && isValidDateTime(trimmedValue) {
|
||||
// Insert !!timestamp tag before the value
|
||||
return beforeColon + ": !!timestamp " + trimmedValue
|
||||
}
|
||||
|
||||
return line
|
||||
}
|
||||
|
||||
// processArrayItemLine processes a line containing an array item
|
||||
func (dtp *DateTimePreprocessor) processArrayItemLine(line string) string {
|
||||
// Find the position of "- " after any leading whitespace
|
||||
trimLeft := strings.TrimLeft(line, " \t")
|
||||
if !strings.HasPrefix(trimLeft, "- ") {
|
||||
return line
|
||||
}
|
||||
|
||||
// Find the actual position of "- " in the original line
|
||||
leadingWhitespace := line[:len(line)-len(trimLeft)]
|
||||
dashIndex := len(leadingWhitespace)
|
||||
|
||||
beforeDash := line[:dashIndex+2] // Include leading whitespace and "- "
|
||||
afterDash := line[dashIndex+2:]
|
||||
|
||||
trimmedValue := strings.TrimSpace(afterDash)
|
||||
|
||||
// Skip if value is empty, quoted, or already complex
|
||||
if trimmedValue == "" ||
|
||||
strings.HasPrefix(trimmedValue, "\"") ||
|
||||
strings.HasPrefix(trimmedValue, "'") ||
|
||||
strings.HasPrefix(trimmedValue, "{") ||
|
||||
strings.HasPrefix(trimmedValue, "[") ||
|
||||
strings.HasPrefix(trimmedValue, "&") ||
|
||||
strings.HasPrefix(trimmedValue, "*") {
|
||||
return line
|
||||
}
|
||||
|
||||
// Check if it matches datetime patterns and is valid
|
||||
if dtp.matchesDateTimePattern(trimmedValue) && isValidDateTime(trimmedValue) {
|
||||
// Insert !!timestamp tag before the value
|
||||
return beforeDash + "!!timestamp " + trimmedValue
|
||||
}
|
||||
|
||||
return line
|
||||
}
|
||||
|
||||
// matchesDateTimePattern checks if a value matches any of the datetime regex patterns
|
||||
func (dtp *DateTimePreprocessor) matchesDateTimePattern(value string) bool {
|
||||
for _, pattern := range dateTimePatterns {
|
||||
if pattern.MatchString(value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
366
pkg/yqlib/datetime_preprocessor_test.go
Normal file
366
pkg/yqlib/datetime_preprocessor_test.go
Normal file
@ -0,0 +1,366 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDateTimePreprocessorBasicFunctionality(t *testing.T) {
|
||||
preprocessor := NewDateTimePreprocessor(true)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "RFC3339 with timezone",
|
||||
input: "timestamp: 2021-01-01T00:00:00Z",
|
||||
expected: "timestamp: !!timestamp 2021-01-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
name: "RFC3339 with offset timezone",
|
||||
input: "timestamp: 2021-01-01T03:10:00+03:00",
|
||||
expected: "timestamp: !!timestamp 2021-01-01T03:10:00+03:00",
|
||||
},
|
||||
{
|
||||
name: "Date only",
|
||||
input: "date: 2021-01-01",
|
||||
expected: "date: !!timestamp 2021-01-01",
|
||||
},
|
||||
{
|
||||
name: "RFC3339 without timezone",
|
||||
input: "timestamp: 2021-01-01T15:04:05",
|
||||
expected: "timestamp: !!timestamp 2021-01-01T15:04:05",
|
||||
},
|
||||
{
|
||||
name: "RFC3339 with milliseconds",
|
||||
input: "timestamp: 2021-01-01T00:00:00.123Z",
|
||||
expected: "timestamp: !!timestamp 2021-01-01T00:00:00.123Z",
|
||||
},
|
||||
{
|
||||
name: "Array item with timestamp",
|
||||
input: "- 2021-01-01T00:00:00Z",
|
||||
expected: "- !!timestamp 2021-01-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
name: "Multiple timestamps in document",
|
||||
input: "start: 2021-01-01T00:00:00Z\nend: 2021-12-31T23:59:59Z",
|
||||
expected: "start: !!timestamp 2021-01-01T00:00:00Z\nend: !!timestamp 2021-12-31T23:59:59Z",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := preprocessor.PreprocessDocument(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("Expected:\n%s\nGot:\n%s", tc.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimePreprocessorDisabled(t *testing.T) {
|
||||
preprocessor := NewDateTimePreprocessor(false)
|
||||
|
||||
input := "timestamp: 2021-01-01T00:00:00Z"
|
||||
expected := "timestamp: 2021-01-01T00:00:00Z"
|
||||
|
||||
result := preprocessor.PreprocessDocument(input)
|
||||
if result != expected {
|
||||
t.Errorf("Expected preprocessing to be disabled. Got: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimePreprocessorSkipsCases(t *testing.T) {
|
||||
preprocessor := NewDateTimePreprocessor(true)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string // Should remain unchanged
|
||||
}{
|
||||
{
|
||||
name: "Already tagged",
|
||||
input: "timestamp: !!timestamp 2021-01-01T00:00:00Z",
|
||||
expected: "timestamp: !!timestamp 2021-01-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
name: "Quoted string",
|
||||
input: "timestamp: \"2021-01-01T00:00:00Z\"",
|
||||
expected: "timestamp: \"2021-01-01T00:00:00Z\"",
|
||||
},
|
||||
{
|
||||
name: "Single quoted string",
|
||||
input: "timestamp: '2021-01-01T00:00:00Z'",
|
||||
expected: "timestamp: '2021-01-01T00:00:00Z'",
|
||||
},
|
||||
{
|
||||
name: "Comment line",
|
||||
input: "# This is 2021-01-01T00:00:00Z",
|
||||
expected: "# This is 2021-01-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
name: "Empty line",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "YAML directive",
|
||||
input: "%YAML 1.1",
|
||||
expected: "%YAML 1.1",
|
||||
},
|
||||
{
|
||||
name: "Document separator",
|
||||
input: "---",
|
||||
expected: "---",
|
||||
},
|
||||
{
|
||||
name: "Document end",
|
||||
input: "...",
|
||||
expected: "...",
|
||||
},
|
||||
{
|
||||
name: "Anchor reference",
|
||||
input: "timestamp: *my_timestamp",
|
||||
expected: "timestamp: *my_timestamp",
|
||||
},
|
||||
{
|
||||
name: "Anchor definition",
|
||||
input: "timestamp: &my_timestamp 2021-01-01T00:00:00Z",
|
||||
expected: "timestamp: &my_timestamp 2021-01-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
name: "Map value",
|
||||
input: "timestamp: {year: 2021, month: 01}",
|
||||
expected: "timestamp: {year: 2021, month: 01}",
|
||||
},
|
||||
{
|
||||
name: "Array value",
|
||||
input: "timestamp: [2021, 01, 01]",
|
||||
expected: "timestamp: [2021, 01, 01]",
|
||||
},
|
||||
{
|
||||
name: "Multi-line scalar literal",
|
||||
input: "description: |",
|
||||
expected: "description: |",
|
||||
},
|
||||
{
|
||||
name: "Multi-line scalar folded",
|
||||
input: "description: >",
|
||||
expected: "description: >",
|
||||
},
|
||||
{
|
||||
name: "Invalid date format",
|
||||
input: "invalid: 2021/01/01",
|
||||
expected: "invalid: 2021/01/01",
|
||||
},
|
||||
{
|
||||
name: "Non-date numeric string",
|
||||
input: "number: 20210101",
|
||||
expected: "number: 20210101",
|
||||
},
|
||||
{
|
||||
name: "Partial date",
|
||||
input: "partial: 2021-01",
|
||||
expected: "partial: 2021-01",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := preprocessor.PreprocessDocument(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("Expected no change. Input: %s, Got: %s", tc.input, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimePreprocessorComplexDocuments(t *testing.T) {
|
||||
preprocessor := NewDateTimePreprocessor(true)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Mixed document with comments and timestamps",
|
||||
input: `# Configuration file
|
||||
version: 1.0
|
||||
created: 2021-01-01T00:00:00Z
|
||||
# Last modified date
|
||||
modified: 2021-12-31T23:59:59Z
|
||||
author: "John Doe"
|
||||
tags:
|
||||
- production
|
||||
- 2021-01-01`,
|
||||
expected: `# Configuration file
|
||||
version: 1.0
|
||||
created: !!timestamp 2021-01-01T00:00:00Z
|
||||
# Last modified date
|
||||
modified: !!timestamp 2021-12-31T23:59:59Z
|
||||
author: "John Doe"
|
||||
tags:
|
||||
- production
|
||||
- !!timestamp 2021-01-01`,
|
||||
},
|
||||
{
|
||||
name: "Nested structures with timestamps",
|
||||
input: `events:
|
||||
start:
|
||||
date: 2021-01-01
|
||||
time: 2021-01-01T09:00:00Z
|
||||
end:
|
||||
date: 2021-01-02
|
||||
time: 2021-01-02T17:00:00Z`,
|
||||
expected: `events:
|
||||
start:
|
||||
date: !!timestamp 2021-01-01
|
||||
time: !!timestamp 2021-01-01T09:00:00Z
|
||||
end:
|
||||
date: !!timestamp 2021-01-02
|
||||
time: !!timestamp 2021-01-02T17:00:00Z`,
|
||||
},
|
||||
{
|
||||
name: "Array of timestamps",
|
||||
input: `dates:
|
||||
- 2021-01-01T00:00:00Z
|
||||
- 2021-06-15T12:30:45Z
|
||||
- 2021-12-31T23:59:59Z`,
|
||||
expected: `dates:
|
||||
- !!timestamp 2021-01-01T00:00:00Z
|
||||
- !!timestamp 2021-06-15T12:30:45Z
|
||||
- !!timestamp 2021-12-31T23:59:59Z`,
|
||||
},
|
||||
{
|
||||
name: "Mixed array with timestamps and other values",
|
||||
input: `mixed:
|
||||
- "string value"
|
||||
- 2021-01-01T00:00:00Z
|
||||
- 42
|
||||
- 2021-12-31`,
|
||||
expected: `mixed:
|
||||
- "string value"
|
||||
- !!timestamp 2021-01-01T00:00:00Z
|
||||
- 42
|
||||
- !!timestamp 2021-12-31`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := preprocessor.PreprocessDocument(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("Expected:\n%s\nGot:\n%s", tc.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimePreprocessorEdgeCases(t *testing.T) {
|
||||
preprocessor := NewDateTimePreprocessor(true)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Whitespace around timestamp",
|
||||
input: "timestamp: 2021-01-01T00:00:00Z ",
|
||||
expected: "timestamp: !!timestamp 2021-01-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
name: "Indented timestamp",
|
||||
input: " timestamp: 2021-01-01T00:00:00Z",
|
||||
expected: " timestamp: !!timestamp 2021-01-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
name: "Deeply indented array item",
|
||||
input: " - 2021-01-01T00:00:00Z",
|
||||
expected: " - !!timestamp 2021-01-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
name: "Empty value",
|
||||
input: "timestamp:",
|
||||
expected: "timestamp:",
|
||||
},
|
||||
{
|
||||
name: "Only key",
|
||||
input: "timestamp",
|
||||
expected: "timestamp",
|
||||
},
|
||||
{
|
||||
name: "Colon in value (not key-value pair)",
|
||||
input: "description: Time is 15:30:45",
|
||||
expected: "description: Time is 15:30:45",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := preprocessor.PreprocessDocument(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("Expected:\n%s\nGot:\n%s", tc.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidDateTime(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{"2021-01-01T00:00:00Z", true},
|
||||
{"2021-01-01T00:00:00+03:00", true},
|
||||
{"2021-01-01T00:00:00", true},
|
||||
{"2021-01-01", true},
|
||||
{"2021-01-01T00:00:00.123Z", true},
|
||||
{"invalid-date", false},
|
||||
{"2021/01/01", false},
|
||||
{"2021-13-01", false},
|
||||
{"2021-01-32", false},
|
||||
{"2021-01-01T25:00:00Z", false},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
result := isValidDateTime(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("isValidDateTime(%s) = %v, expected %v", tc.input, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchesDateTimePattern(t *testing.T) {
|
||||
preprocessor := NewDateTimePreprocessor(true)
|
||||
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{"2021-01-01T00:00:00Z", true},
|
||||
{"2021-01-01T00:00:00+03:00", true},
|
||||
{"2021-01-01T00:00:00", true},
|
||||
{"2021-01-01", true},
|
||||
{"2021-01-01T00:00:00.123Z", true},
|
||||
{" 2021-01-01T00:00:00Z ", true}, // with whitespace
|
||||
{"not-a-date", false},
|
||||
{"2021/01/01", false},
|
||||
{"15:30:45", false}, // time only
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
result := preprocessor.matchesDateTimePattern(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("matchesDateTimePattern(%s) = %v, expected %v", tc.input, result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -40,6 +40,9 @@ type goccyYamlDecoder struct {
|
||||
// It is reset by Init() for each new stream.
|
||||
anchorMap map[string]*CandidateNode
|
||||
|
||||
// dateTimePreprocessor handles automatic timestamp tagging for datetime arithmetic compatibility
|
||||
dateTimePreprocessor *DateTimePreprocessor
|
||||
|
||||
readAnything bool // Flag to track if any actual YAML node (or synthesized comment node) has been decoded.
|
||||
firstFile bool // Flag for 'evaluateTogether' mode to handle leading content only once.
|
||||
documentIndex uint // Index of the current document within the stream.
|
||||
@ -48,7 +51,11 @@ type goccyYamlDecoder struct {
|
||||
// NewGoccyYAMLDecoder creates a new YAML decoder using the goccy/go-yaml library,
|
||||
// configured with the given preferences.
|
||||
func NewGoccyYAMLDecoder(prefs YamlPreferences) Decoder {
|
||||
return &goccyYamlDecoder{prefs: prefs, firstFile: true}
|
||||
return &goccyYamlDecoder{
|
||||
prefs: prefs,
|
||||
firstFile: true,
|
||||
dateTimePreprocessor: NewDateTimePreprocessor(true), // Enable datetime preprocessing for Goccy
|
||||
}
|
||||
}
|
||||
|
||||
// processReadStream pre-processes the input stream to extract leading comments,
|
||||
@ -129,9 +136,29 @@ func (dec *goccyYamlDecoder) Init(reader io.Reader) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply datetime preprocessing to the remaining content
|
||||
if dec.dateTimePreprocessor != nil {
|
||||
remainingBytes, err := io.ReadAll(readerToUse)
|
||||
if err != nil && errors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
preprocessedContent := dec.dateTimePreprocessor.PreprocessDocument(string(remainingBytes))
|
||||
readerToUse = strings.NewReader(preprocessedContent)
|
||||
}
|
||||
} else if !dec.prefs.LeadingContentPreProcessing {
|
||||
// If not preprocessing, TeeReader captures initial bytes in case it's a comment-only document.
|
||||
readerToUse = io.TeeReader(reader, &dec.bufferRead)
|
||||
if dec.dateTimePreprocessor != nil {
|
||||
// Read all content, apply datetime preprocessing, then create new reader
|
||||
allBytes, err := io.ReadAll(reader)
|
||||
if err != nil && errors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
preprocessedContent := dec.dateTimePreprocessor.PreprocessDocument(string(allBytes))
|
||||
readerToUse = io.TeeReader(strings.NewReader(preprocessedContent), &dec.bufferRead)
|
||||
} else {
|
||||
readerToUse = io.TeeReader(reader, &dec.bufferRead)
|
||||
}
|
||||
}
|
||||
|
||||
dec.leadingContent = processedLeadingContent
|
||||
|
||||
@ -428,21 +428,9 @@ var addOperatorScenarios = []expressionScenario{
|
||||
},
|
||||
}
|
||||
|
||||
func testAddScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
|
||||
// Skip datetime arithmetic tests for goccy as it requires explicit timestamp tagging
|
||||
// while yaml.v3 auto-detects ISO8601 strings as timestamps
|
||||
if ConfiguredYamlPreferences.UseGoccyParser {
|
||||
if s.description == "Date addition" || s.description == "Date addition -date only" {
|
||||
t.Skip("goccy parser requires explicit timestamp tagging for datetime arithmetic - more YAML spec compliant")
|
||||
return
|
||||
}
|
||||
}
|
||||
testScenario(t, s)
|
||||
}
|
||||
|
||||
func TestAddOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range addOperatorScenarios {
|
||||
testAddScenarioWithParserCheck(t, &tt)
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "add", addOperatorScenarios)
|
||||
}
|
||||
|
||||
@ -60,6 +60,7 @@ var collectOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!seq)::- {a: apple}\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
@ -136,18 +137,9 @@ var collectOperatorScenarios = []expressionScenario{
|
||||
},
|
||||
}
|
||||
|
||||
func testCollectScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
|
||||
// Skip comment-related tests for goccy as it handles comment placement more strictly
|
||||
if s.description == "with comments" && ConfiguredYamlPreferences.UseGoccyParser {
|
||||
t.Skip("goccy parser handles trailing comments more strictly - structurally equivalent but different comment handling")
|
||||
return
|
||||
}
|
||||
testScenario(t, s)
|
||||
}
|
||||
|
||||
func TestCollectOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range collectOperatorScenarios {
|
||||
testCollectScenarioWithParserCheck(t, &tt)
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "collect-into-array", collectOperatorScenarios)
|
||||
}
|
||||
|
||||
@ -62,6 +62,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: cat # single\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Set line comment of a maps/arrays",
|
||||
@ -71,6 +72,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: # single\n b: things\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
@ -79,6 +81,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: cat # dog\nb: dog\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
@ -88,6 +91,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
"D0, P[], (!!map)::a: cat # 0\n",
|
||||
"D1, P[], (!!map)::a: dog # 1\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Use update assign to perform relative updates",
|
||||
@ -96,6 +100,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: cat # cat\nb: dog # dog\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
@ -104,6 +109,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: cat # cat\n# cat\n\n# cat\nb: dog # dog\n# dog\n\n# dog\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Where is the comment - map key example",
|
||||
@ -113,6 +119,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
expectedWhereIsMyCommentMapKey,
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Retrieve comment - map key example",
|
||||
@ -122,6 +129,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[hello], (!!str)::hello-world-comment\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Where is the comment - array example",
|
||||
@ -131,6 +139,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
expectedWhereIsMyCommentArray,
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Retrieve comment - array example",
|
||||
@ -140,6 +149,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[name 0], (!!str)::under-name-comment\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Set head comment",
|
||||
@ -148,6 +158,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::# single\na: cat\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Set head comment of a map entry",
|
||||
@ -156,6 +167,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::f: foo\n# single\na:\n b: cat\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Set foot comment, using an expression",
|
||||
@ -164,6 +176,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: cat\n# cat\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
@ -173,6 +186,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: cat\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
@ -181,6 +195,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: cat\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
@ -189,6 +204,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: cat\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Remove comment",
|
||||
@ -197,6 +213,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: cat\nb: dog # leave this\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Remove (strip) all comments",
|
||||
@ -206,6 +223,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: cat\nb:\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Get line comment",
|
||||
@ -214,6 +232,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[a], (!!str)::meow\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Get head comment",
|
||||
@ -223,6 +242,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!str)::welcome!\n\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
@ -232,6 +252,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: cat\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
@ -241,6 +262,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: cat\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Head comment with document split",
|
||||
@ -250,6 +272,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!str)::welcome!\nbob\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Get foot comment",
|
||||
@ -259,6 +282,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!str)::have a great day\nno really\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "leading spaces",
|
||||
@ -268,6 +292,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!null):: # hi\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "string spaces",
|
||||
@ -277,6 +302,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!str)::# hi\ncat\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "leading spaces with new line",
|
||||
@ -286,6 +312,7 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!null):: # hi\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "directive",
|
||||
@ -295,22 +322,90 @@ var commentOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!null)::%YAML 1.1\n# hi\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
}
|
||||
|
||||
func testCommentScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
|
||||
// Skip comment tests for goccy as it handles comment placement and formatting differently
|
||||
// The structural data is preserved but comment positioning varies between parsers
|
||||
if ConfiguredYamlPreferences.UseGoccyParser {
|
||||
t.Skip("goccy parser handles comment placement and formatting differently - data integrity preserved")
|
||||
return
|
||||
}
|
||||
testScenario(t, s)
|
||||
// Goccy-specific comment operator scenarios - these validate the actual behaviour of Goccy parser
|
||||
var goccyCommentOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
description: "Goccy: Set line comment - basic functionality",
|
||||
subdescription: "Goccy parser maintains comment functionality with potentially different placement",
|
||||
document: `a: cat`,
|
||||
expression: `.a line_comment="single"`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: cat # single\n",
|
||||
},
|
||||
skipForYamlV3: true,
|
||||
},
|
||||
{
|
||||
description: "Goccy: Get line comment - basic functionality",
|
||||
subdescription: "Goccy parser can read existing line comments",
|
||||
document: "a: cat # meow",
|
||||
expression: `.a | line_comment`,
|
||||
expected: []string{
|
||||
"D0, P[a], (!!str)::meow\n",
|
||||
},
|
||||
skipForYamlV3: true,
|
||||
},
|
||||
{
|
||||
description: "Goccy: Set head comment - basic functionality",
|
||||
subdescription: "Goccy parser can set head comments",
|
||||
document: `a: cat`,
|
||||
expression: `. head_comment="header"`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::# header\na: cat\n",
|
||||
},
|
||||
skipForYamlV3: true,
|
||||
},
|
||||
{
|
||||
description: "Goccy: Get head comment - basic functionality",
|
||||
subdescription: "Goccy parser can read existing head comments",
|
||||
document: "# welcome!\na: cat",
|
||||
expression: `. | head_comment`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!str)::welcome!\n",
|
||||
},
|
||||
skipForYamlV3: true,
|
||||
},
|
||||
{
|
||||
description: "Goccy: Set foot comment - basic functionality",
|
||||
subdescription: "Goccy parser can set foot comments",
|
||||
document: `a: cat`,
|
||||
expression: `. foot_comment="footer"`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: cat\n# footer\n",
|
||||
},
|
||||
skipForYamlV3: true,
|
||||
},
|
||||
{
|
||||
description: "Goccy: Remove line comment",
|
||||
subdescription: "Goccy parser can remove comments",
|
||||
document: "a: cat # remove_me\nb: dog # keep_me",
|
||||
expression: `.a line_comment=""`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: cat\nb: dog # keep_me\n",
|
||||
},
|
||||
skipForYamlV3: true,
|
||||
},
|
||||
{
|
||||
description: "Goccy: Comment preservation during data operations",
|
||||
subdescription: "Goccy parser preserves structural integrity while handling comments",
|
||||
document: "# header\na: cat # inline\nb: dog\n# footer",
|
||||
expression: `.c = "new"`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::# header\na: cat # inline\nb: dog\nc: new\n# footer\n",
|
||||
},
|
||||
skipForYamlV3: true,
|
||||
},
|
||||
}
|
||||
|
||||
func TestCommentOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range commentOperatorScenarios {
|
||||
testCommentScenarioWithParserCheck(t, &tt)
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
for _, tt := range goccyCommentOperatorScenarios {
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "comment-operators", commentOperatorScenarios)
|
||||
}
|
||||
|
||||
@ -128,19 +128,9 @@ var dateTimeOperatorScenarios = []expressionScenario{
|
||||
},
|
||||
}
|
||||
|
||||
func testDatetimeScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
|
||||
// Skip datetime arithmetic tests for goccy as it requires explicit timestamp tagging
|
||||
// while yaml.v3 auto-detects ISO8601 strings as timestamps
|
||||
if (s.description == "Date addition" || s.description == "Date subtraction") && ConfiguredYamlPreferences.UseGoccyParser {
|
||||
t.Skip("goccy parser requires explicit timestamp tagging for datetime arithmetic - more YAML spec compliant")
|
||||
return
|
||||
}
|
||||
testScenario(t, s)
|
||||
}
|
||||
|
||||
func TestDatetimeOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range dateTimeOperatorScenarios {
|
||||
testDatetimeScenarioWithParserCheck(t, &tt)
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "datetime", dateTimeOperatorScenarios)
|
||||
}
|
||||
|
||||
@ -134,21 +134,13 @@ var entriesOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::# abc\na: {b: bird}\n# xyz\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
}
|
||||
|
||||
func testEntriesScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
|
||||
// Skip comment-related tests for goccy as it handles comment placement more strictly
|
||||
if s.description == "Use with_entries to filter the map; head comment" && ConfiguredYamlPreferences.UseGoccyParser {
|
||||
t.Skip("goccy parser handles trailing comments more strictly - structurally equivalent but different comment handling")
|
||||
return
|
||||
}
|
||||
testScenario(t, s)
|
||||
}
|
||||
|
||||
func TestEntriesOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range entriesOperatorScenarios {
|
||||
testEntriesScenarioWithParserCheck(t, &tt)
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "entries", entriesOperatorScenarios)
|
||||
}
|
||||
|
||||
@ -169,21 +169,13 @@ var envOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::# abc\n{v: \"cat meow\"}\n# xyz\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
}
|
||||
|
||||
func testEnvScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
|
||||
// Skip comment-related tests for goccy as it handles comment placement more strictly
|
||||
if s.description == "with header/footer" && ConfiguredYamlPreferences.UseGoccyParser {
|
||||
t.Skip("goccy parser handles trailing comments more strictly - structurally equivalent but different comment handling")
|
||||
return
|
||||
}
|
||||
testScenario(t, s)
|
||||
}
|
||||
|
||||
func TestEnvOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range envOperatorScenarios {
|
||||
testEnvScenarioWithParserCheck(t, &tt)
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "env-variable-operators", envOperatorScenarios)
|
||||
}
|
||||
|
||||
@ -110,6 +110,7 @@ var keysOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[a x], (!!str)::comment on key\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Check node is a key",
|
||||
@ -121,18 +122,9 @@ var keysOperatorScenarios = []expressionScenario{
|
||||
},
|
||||
}
|
||||
|
||||
func testKeysScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
|
||||
// Skip comment-related tests for goccy as it handles comment placement differently
|
||||
if s.description == "Get comment from map key" && ConfiguredYamlPreferences.UseGoccyParser {
|
||||
t.Skip("goccy parser handles comment placement differently - data integrity preserved")
|
||||
return
|
||||
}
|
||||
testScenario(t, s)
|
||||
}
|
||||
|
||||
func TestKeysOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range keysOperatorScenarios {
|
||||
testKeysScenarioWithParserCheck(t, &tt)
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "keys", keysOperatorScenarios)
|
||||
}
|
||||
|
||||
@ -94,6 +94,7 @@ var multiplyOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::sample:\n - &a\n - !!merge <<: *a\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
@ -112,6 +113,7 @@ var multiplyOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::# here\n\na: apple\nb: banana\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
@ -121,6 +123,7 @@ var multiplyOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[node], (!!map)::# here\na: apple\nb: banana\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
@ -130,6 +133,7 @@ var multiplyOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::# here\n\nb: banana\na: apple\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
@ -139,6 +143,7 @@ var multiplyOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::b: banana\n# here\na: apple\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
@ -148,6 +153,7 @@ var multiplyOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a: apple\nb: banana\n# footer\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
@ -157,6 +163,7 @@ var multiplyOperatorScenarios = []expressionScenario{
|
||||
expected: []string{ // not sure why there's an extra newline *shrug*
|
||||
"D0, P[], (!!map)::a: apple\n# footer\n\nb: banana\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "doc 2 has footer",
|
||||
@ -167,6 +174,7 @@ var multiplyOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::b: banana\na: apple\n# footer\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Multiply integers",
|
||||
@ -667,30 +675,36 @@ var multiplyOperatorScenarios = []expressionScenario{
|
||||
},
|
||||
}
|
||||
|
||||
func testMultiplyScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
|
||||
// Skip tests where goccy correctly rejects invalid YAML at parse time
|
||||
if ConfiguredYamlPreferences.UseGoccyParser {
|
||||
// Skip merge anchor tests - goccy correctly rejects merge anchors with null values
|
||||
if s.document == mergeArrayWithAnchors {
|
||||
t.Skip("goccy parser correctly rejects merge anchors with null values at parse time")
|
||||
return
|
||||
}
|
||||
|
||||
// Skip comment-related tests - goccy handles comment placement differently
|
||||
if s.document == docWithHeader || s.document == nodeWithHeader ||
|
||||
s.document2 == docWithHeader || s.document2 == nodeWithHeader ||
|
||||
s.document == docWithFooter || s.document == nodeWithFooter ||
|
||||
s.document2 == docWithFooter || s.document2 == nodeWithFooter {
|
||||
t.Skip("goccy parser handles comment placement differently - data integrity preserved")
|
||||
return
|
||||
}
|
||||
}
|
||||
testScenario(t, s)
|
||||
// Goccy-specific multiply operator scenarios
|
||||
var goccyMultiplyOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
description: "Goccy: Basic merge operation preserves structure",
|
||||
subdescription: "Goccy parser maintains structural integrity during merge operations",
|
||||
document: `{a: apple, b: banana}`,
|
||||
expression: `. * {c: cherry}`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::{a: apple, b: banana, c: cherry}\n",
|
||||
},
|
||||
skipForYamlV3: true, // different comment handling behaviour
|
||||
},
|
||||
{
|
||||
description: "Goccy: Merge with valid anchor structure",
|
||||
subdescription: "Goccy handles valid YAML anchor structures correctly",
|
||||
document: `{a: &anchor value, b: *anchor}`,
|
||||
expression: `. * {c: new}`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::{a: &anchor value, b: *anchor, c: new}\n",
|
||||
},
|
||||
skipForYamlV3: true, // different anchor handling nuances
|
||||
},
|
||||
}
|
||||
|
||||
func TestMultiplyOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range multiplyOperatorScenarios {
|
||||
testMultiplyScenarioWithParserCheck(t, &tt)
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
for _, tt := range goccyMultiplyOperatorScenarios {
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "multiply-merge", multiplyOperatorScenarios)
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ var omitOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::# abc\nmyMap: {dog: bark, thing: hamster}\n# xyz\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Omit indices from array",
|
||||
@ -59,23 +60,13 @@ var omitOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!seq)::# abc\n[leopard]\n# xyz\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
}
|
||||
|
||||
func testOmitScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
|
||||
// Skip comment-related tests for goccy as it handles comment placement more strictly
|
||||
if ConfiguredYamlPreferences.UseGoccyParser {
|
||||
if s.description == "Omit keys from map with comments" || s.description == "Omit indices from array with comments" {
|
||||
t.Skip("goccy parser handles trailing comments more strictly - structurally equivalent but different comment handling")
|
||||
return
|
||||
}
|
||||
}
|
||||
testScenario(t, s)
|
||||
}
|
||||
|
||||
func TestOmitOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range omitOperatorScenarios {
|
||||
testOmitScenarioWithParserCheck(t, &tt)
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "omit", omitOperatorScenarios)
|
||||
}
|
||||
|
||||
@ -50,6 +50,7 @@ var pickOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::# abc\nmyMap: {hamster: squeak, cat: meow}\n# xyz\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Pick indices from array",
|
||||
@ -68,23 +69,13 @@ var pickOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!seq)::# abc\n[lion, cat]\n# xyz\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
}
|
||||
|
||||
func testPickScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
|
||||
// Skip comment-related tests for goccy as it handles comment placement more strictly
|
||||
if ConfiguredYamlPreferences.UseGoccyParser {
|
||||
if s.description == "Pick keys from map with comments" || s.description == "Pick indices from array with comments" {
|
||||
t.Skip("goccy parser handles trailing comments more strictly - structurally equivalent but different comment handling")
|
||||
return
|
||||
}
|
||||
}
|
||||
testScenario(t, s)
|
||||
}
|
||||
|
||||
func TestPickOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range pickOperatorScenarios {
|
||||
testPickScenarioWithParserCheck(t, &tt)
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "pick", pickOperatorScenarios)
|
||||
}
|
||||
|
||||
@ -62,21 +62,13 @@ var reverseOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!seq)::# abc\n[{a: cat}, {a: banana}, {a: apple}]\n# xyz\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
}
|
||||
|
||||
func testReverseScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
|
||||
// Skip comment-related tests for goccy as it handles comment placement more strictly
|
||||
if s.description == "Sort descending by string field, with comments" && ConfiguredYamlPreferences.UseGoccyParser {
|
||||
t.Skip("goccy parser handles trailing comments more strictly - structurally equivalent but different comment handling")
|
||||
return
|
||||
}
|
||||
testScenario(t, s)
|
||||
}
|
||||
|
||||
func TestReverseOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range reverseOperatorScenarios {
|
||||
testReverseScenarioWithParserCheck(t, &tt)
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "reverse", reverseOperatorScenarios)
|
||||
}
|
||||
|
||||
@ -170,21 +170,13 @@ var sortByOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!seq)::# abc\n- def\n# ghi\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
}
|
||||
|
||||
func testSortScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
|
||||
// Skip trailing comment test for goccy as it handles comments more strictly
|
||||
if s.description == "head comment" && ConfiguredYamlPreferences.UseGoccyParser {
|
||||
t.Skip("goccy parser handles trailing comments differently - structurally equivalent but different comment placement")
|
||||
return
|
||||
}
|
||||
testScenario(t, s)
|
||||
}
|
||||
|
||||
func TestSortByOperatorScenarios(t *testing.T) {
|
||||
func TestSortOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range sortByOperatorScenarios {
|
||||
testSortScenarioWithParserCheck(t, &tt)
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "sort", sortByOperatorScenarios)
|
||||
}
|
||||
|
||||
@ -52,6 +52,7 @@ var styleOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::'bing': &foo {'x': 'z'}\n'a':\n 'c': 'cat'\n !!merge <<: [*foo]\n",
|
||||
},
|
||||
skipForGoccy: true,
|
||||
},
|
||||
{
|
||||
description: "Set single quote style",
|
||||
@ -169,21 +170,9 @@ g:
|
||||
},
|
||||
}
|
||||
|
||||
func testStyleScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
|
||||
// Skip tests where goccy correctly rejects invalid YAML at parse time
|
||||
if ConfiguredYamlPreferences.UseGoccyParser {
|
||||
// Check if the document contains merge anchor with sequence (invalid YAML)
|
||||
if s.document == "bing: &foo frog\na:\n c: cat\n <<: [*foo]" {
|
||||
t.Skip("goccy parser correctly rejects merge anchors with sequences at parse time")
|
||||
return
|
||||
}
|
||||
}
|
||||
testScenario(t, s)
|
||||
}
|
||||
|
||||
func TestStyleOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range styleOperatorScenarios {
|
||||
testStyleScenarioWithParserCheck(t, &tt)
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "style", styleOperatorScenarios)
|
||||
}
|
||||
|
||||
@ -159,21 +159,9 @@ var subtractOperatorScenarios = []expressionScenario{
|
||||
},
|
||||
}
|
||||
|
||||
func testSubtractScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
|
||||
// Skip datetime arithmetic tests for goccy as it requires explicit timestamp tagging
|
||||
// while yaml.v3 auto-detects ISO8601 strings as timestamps
|
||||
if ConfiguredYamlPreferences.UseGoccyParser {
|
||||
if s.description == "Date subtraction" || s.description == "Date subtraction - only date" {
|
||||
t.Skip("goccy parser requires explicit timestamp tagging for datetime arithmetic - more YAML spec compliant")
|
||||
return
|
||||
}
|
||||
}
|
||||
testScenario(t, s)
|
||||
}
|
||||
|
||||
func TestSubtractOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range subtractOperatorScenarios {
|
||||
testSubtractScenarioWithParserCheck(t, &tt)
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "subtract", subtractOperatorScenarios)
|
||||
}
|
||||
|
||||
@ -558,24 +558,9 @@ var traversePathOperatorScenarios = []expressionScenario{
|
||||
},
|
||||
}
|
||||
|
||||
func testTraverseScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
|
||||
// Skip tests where goccy correctly rejects invalid YAML at parse time
|
||||
if ConfiguredYamlPreferences.UseGoccyParser {
|
||||
if s.description == "strange map with key but no value" {
|
||||
t.Skip("goccy parser correctly rejects malformed YAML with explicit null tag followed by sequence")
|
||||
return
|
||||
}
|
||||
if s.expectedError == "can only use merge anchors with maps (!!map), but got !!seq" {
|
||||
t.Skip("goccy parser correctly rejects merge anchors with sequences at parse time")
|
||||
return
|
||||
}
|
||||
}
|
||||
testScenario(t, s)
|
||||
}
|
||||
|
||||
func TestTraversePathOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range traversePathOperatorScenarios {
|
||||
testTraverseScenarioWithParserCheck(t, &tt)
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "traverse-read", traversePathOperatorScenarios)
|
||||
}
|
||||
|
||||
@ -103,18 +103,9 @@ var uniqueOperatorScenarios = []expressionScenario{
|
||||
},
|
||||
}
|
||||
|
||||
func testUniqueScenarioWithParserCheck(t *testing.T, s *expressionScenario) {
|
||||
// Skip comment-related tests for goccy as it handles comment placement more strictly
|
||||
if s.document == "# abc\n[{name: harry, pet: cat}, {pet: fish}, {name: harry, pet: dog}]\n# xyz" && ConfiguredYamlPreferences.UseGoccyParser {
|
||||
t.Skip("goccy parser handles trailing comments more strictly - structurally equivalent but different comment handling")
|
||||
return
|
||||
}
|
||||
testScenario(t, s)
|
||||
}
|
||||
|
||||
func TestUniqueOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range uniqueOperatorScenarios {
|
||||
testUniqueScenarioWithParserCheck(t, &tt)
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "unique", uniqueOperatorScenarios)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user