193 lines
5.0 KiB
Go
193 lines
5.0 KiB
Go
// Copyright 2025 The go-yaml Project Contributors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// YAML test data loading utilities.
|
|
// Provides helper functions for loading and processing YAML test data,
|
|
// including scalar coercion.
|
|
|
|
package libyaml
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
)
|
|
|
|
// coerceScalar converts a YAML scalar string to an appropriate Go type
|
|
func coerceScalar(value string) any {
|
|
// Try bool and null
|
|
switch value {
|
|
case "true":
|
|
return true
|
|
case "false":
|
|
return false
|
|
case "null":
|
|
return nil
|
|
}
|
|
|
|
// Try hex int (0x or 0X prefix) - needed for test data byte arrays
|
|
var intVal int
|
|
if _, err := fmt.Sscanf(strings.ToLower(value), "0x%x", &intVal); err == nil {
|
|
return intVal
|
|
}
|
|
|
|
// Try float (must check before int because %d will parse "1.5" as "1")
|
|
if strings.Contains(value, ".") {
|
|
var floatVal float64
|
|
if _, err := fmt.Sscanf(value, "%f", &floatVal); err == nil {
|
|
return floatVal
|
|
}
|
|
}
|
|
|
|
// Try decimal int - use int64 to handle large values on 32-bit systems
|
|
var int64Val int64
|
|
if _, err := fmt.Sscanf(value, "%d", &int64Val); err == nil {
|
|
// Return as int if it fits, otherwise int64
|
|
if int64Val == int64(int(int64Val)) {
|
|
return int(int64Val)
|
|
}
|
|
return int64Val
|
|
}
|
|
|
|
// Default to string
|
|
return value
|
|
}
|
|
|
|
// LoadYAML parses YAML data using the native libyaml Parser.
|
|
// This function is exported so it can be used by other packages for data-driven testing.
|
|
// It returns a generic interface{} which is typically:
|
|
// - map[string]interface{} for YAML mappings
|
|
// - []interface{} for YAML sequences
|
|
// - scalar values, resolved according to the following rules:
|
|
// - Booleans: "true" and "false" are returned as bool (true/false).
|
|
// - Nulls: "null" is returned as nil.
|
|
// - Floats: values containing "." are parsed as float64.
|
|
// - Decimal integers: values matching integer format are parsed as int.
|
|
// - All other values are returned as string.
|
|
//
|
|
// This scalar resolution behavior matches the implementation in coerceScalar.
|
|
func LoadYAML(data []byte) (any, error) {
|
|
parser := NewParser()
|
|
parser.SetInputString(data)
|
|
defer parser.Delete()
|
|
|
|
type stackEntry struct {
|
|
container any // map[string]interface{} or []interface{}
|
|
key string // for maps: current key waiting for value
|
|
}
|
|
|
|
var stack []stackEntry
|
|
var root any
|
|
|
|
for {
|
|
var event Event
|
|
if err := parser.Parse(&event); err != nil {
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
switch event.Type {
|
|
case STREAM_END_EVENT:
|
|
// End of stream, we're done
|
|
return root, nil
|
|
|
|
case STREAM_START_EVENT, DOCUMENT_START_EVENT:
|
|
// Structural markers, no action needed
|
|
|
|
case MAPPING_START_EVENT:
|
|
newMap := make(map[string]any)
|
|
stack = append(stack, stackEntry{container: newMap})
|
|
|
|
case MAPPING_END_EVENT:
|
|
if len(stack) > 0 {
|
|
popped := stack[len(stack)-1]
|
|
stack = stack[:len(stack)-1]
|
|
|
|
// Add completed map to parent or set as root
|
|
if len(stack) == 0 {
|
|
root = popped.container
|
|
} else {
|
|
parent := &stack[len(stack)-1]
|
|
if m, ok := parent.container.(map[string]any); ok {
|
|
m[parent.key] = popped.container
|
|
parent.key = "" // Reset key after use
|
|
} else if s, ok := parent.container.([]any); ok {
|
|
parent.container = append(s, popped.container)
|
|
}
|
|
}
|
|
}
|
|
|
|
case SEQUENCE_START_EVENT:
|
|
newSlice := make([]any, 0)
|
|
stack = append(stack, stackEntry{container: newSlice})
|
|
|
|
case SEQUENCE_END_EVENT:
|
|
if len(stack) > 0 {
|
|
popped := stack[len(stack)-1]
|
|
stack = stack[:len(stack)-1]
|
|
|
|
// Add completed slice to parent or set as root
|
|
if len(stack) == 0 {
|
|
root = popped.container
|
|
} else {
|
|
parent := &stack[len(stack)-1]
|
|
if m, ok := parent.container.(map[string]any); ok {
|
|
m[parent.key] = popped.container
|
|
parent.key = "" // Reset key after use
|
|
} else if s, ok := parent.container.([]any); ok {
|
|
parent.container = append(s, popped.container)
|
|
}
|
|
}
|
|
}
|
|
|
|
case SCALAR_EVENT:
|
|
value := string(event.Value)
|
|
// Only coerce plain (unquoted) scalars
|
|
isQuoted := ScalarStyle(event.Style) != PLAIN_SCALAR_STYLE
|
|
|
|
if len(stack) == 0 {
|
|
// Scalar at root level
|
|
if isQuoted {
|
|
root = value
|
|
} else {
|
|
root = coerceScalar(value)
|
|
}
|
|
} else {
|
|
parent := &stack[len(stack)-1]
|
|
if m, ok := parent.container.(map[string]any); ok {
|
|
if parent.key == "" {
|
|
// This scalar is a key - keep as string, don't coerce
|
|
parent.key = value
|
|
} else {
|
|
// This scalar is a value
|
|
if isQuoted {
|
|
m[parent.key] = value
|
|
} else {
|
|
m[parent.key] = coerceScalar(value)
|
|
}
|
|
parent.key = ""
|
|
}
|
|
} else if s, ok := parent.container.([]any); ok {
|
|
// Add to sequence
|
|
if isQuoted {
|
|
parent.container = append(s, value)
|
|
} else {
|
|
parent.container = append(s, coerceScalar(value))
|
|
}
|
|
}
|
|
}
|
|
|
|
case DOCUMENT_END_EVENT:
|
|
// Document end marker, continue processing
|
|
|
|
case ALIAS_EVENT, TAIL_COMMENT_EVENT:
|
|
// For now, skip aliases and comments (not used in test data)
|
|
}
|
|
}
|
|
|
|
return root, nil
|
|
}
|