211 lines
5.6 KiB
Go
211 lines
5.6 KiB
Go
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package yamlutils
|
|
|
|
import (
|
|
json "encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"github.com/go-openapi/swag/jsonutils"
|
|
yaml "go.yaml.in/yaml/v3"
|
|
)
|
|
|
|
// YAMLToJSON converts a YAML document into JSON bytes.
|
|
//
|
|
// Note: a YAML document is the output from a [yaml.Marshaler], e.g a pointer to a [yaml.Node].
|
|
//
|
|
// [YAMLToJSON] is typically called after [BytesToYAMLDoc].
|
|
func YAMLToJSON(value any) (json.RawMessage, error) {
|
|
jm, err := transformData(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b, err := jsonutils.WriteJSON(jm)
|
|
|
|
return json.RawMessage(b), err
|
|
}
|
|
|
|
// BytesToYAMLDoc converts a byte slice into a YAML document.
|
|
//
|
|
// This function only supports root documents that are objects.
|
|
//
|
|
// A YAML document is a pointer to a [yaml.Node].
|
|
func BytesToYAMLDoc(data []byte) (any, error) {
|
|
var document yaml.Node // preserve order that is present in the document
|
|
if err := yaml.Unmarshal(data, &document); err != nil {
|
|
return nil, err
|
|
}
|
|
if document.Kind != yaml.DocumentNode || len(document.Content) != 1 || document.Content[0].Kind != yaml.MappingNode {
|
|
return nil, fmt.Errorf("only YAML documents that are objects are supported: %w", ErrYAML)
|
|
}
|
|
return &document, nil
|
|
}
|
|
|
|
func yamlNode(root *yaml.Node) (any, error) {
|
|
switch root.Kind {
|
|
case yaml.DocumentNode:
|
|
return yamlDocument(root)
|
|
case yaml.SequenceNode:
|
|
return yamlSequence(root)
|
|
case yaml.MappingNode:
|
|
return yamlMapping(root)
|
|
case yaml.ScalarNode:
|
|
return yamlScalar(root)
|
|
case yaml.AliasNode:
|
|
return yamlNode(root.Alias)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported YAML node type: %v: %w", root.Kind, ErrYAML)
|
|
}
|
|
}
|
|
|
|
func yamlDocument(node *yaml.Node) (any, error) {
|
|
if len(node.Content) != 1 {
|
|
return nil, fmt.Errorf("unexpected YAML Document node content length: %d: %w", len(node.Content), ErrYAML)
|
|
}
|
|
return yamlNode(node.Content[0])
|
|
}
|
|
|
|
func yamlMapping(node *yaml.Node) (any, error) {
|
|
const sensibleAllocDivider = 2 // nodes concatenate (key,value) sequences
|
|
m := make(YAMLMapSlice, len(node.Content)/sensibleAllocDivider)
|
|
|
|
if err := m.UnmarshalYAML(node); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func yamlSequence(node *yaml.Node) (any, error) {
|
|
s := make([]any, 0)
|
|
|
|
for i := range len(node.Content) {
|
|
v, err := yamlNode(node.Content[i])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to decode YAML sequence value: %w: %w", err, ErrYAML)
|
|
}
|
|
s = append(s, v)
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
const ( // See https://yaml.org/type/
|
|
yamlStringScalar = "tag:yaml.org,2002:str"
|
|
yamlIntScalar = "tag:yaml.org,2002:int"
|
|
yamlBoolScalar = "tag:yaml.org,2002:bool"
|
|
yamlFloatScalar = "tag:yaml.org,2002:float"
|
|
yamlTimestamp = "tag:yaml.org,2002:timestamp"
|
|
yamlNull = "tag:yaml.org,2002:null"
|
|
)
|
|
|
|
func yamlScalar(node *yaml.Node) (any, error) {
|
|
switch node.LongTag() {
|
|
case yamlStringScalar:
|
|
return node.Value, nil
|
|
case yamlBoolScalar:
|
|
b, err := strconv.ParseBool(node.Value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to process scalar node. Got %q. Expecting bool content: %w: %w", node.Value, err, ErrYAML)
|
|
}
|
|
return b, nil
|
|
case yamlIntScalar:
|
|
i, err := strconv.ParseInt(node.Value, 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to process scalar node. Got %q. Expecting integer content: %w: %w", node.Value, err, ErrYAML)
|
|
}
|
|
return i, nil
|
|
case yamlFloatScalar:
|
|
f, err := strconv.ParseFloat(node.Value, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to process scalar node. Got %q. Expecting float content: %w: %w", node.Value, err, ErrYAML)
|
|
}
|
|
return f, nil
|
|
case yamlTimestamp:
|
|
// YAML timestamp is marshaled as string, not time
|
|
return node.Value, nil
|
|
case yamlNull:
|
|
return nil, nil //nolint:nilnil
|
|
default:
|
|
return nil, fmt.Errorf("YAML tag %q is not supported: %w", node.LongTag(), ErrYAML)
|
|
}
|
|
}
|
|
|
|
func yamlStringScalarC(node *yaml.Node) (string, error) {
|
|
if node.Kind != yaml.ScalarNode {
|
|
return "", fmt.Errorf("expecting a string scalar but got %q: %w", node.Kind, ErrYAML)
|
|
}
|
|
switch node.LongTag() {
|
|
case yamlStringScalar, yamlIntScalar, yamlFloatScalar:
|
|
return node.Value, nil
|
|
default:
|
|
return "", fmt.Errorf("YAML tag %q is not supported as map key: %w", node.LongTag(), ErrYAML)
|
|
}
|
|
}
|
|
|
|
func format(t any) (string, error) {
|
|
switch k := t.(type) {
|
|
case string:
|
|
return k, nil
|
|
case uint:
|
|
return strconv.FormatUint(uint64(k), 10), nil
|
|
case uint8:
|
|
return strconv.FormatUint(uint64(k), 10), nil
|
|
case uint16:
|
|
return strconv.FormatUint(uint64(k), 10), nil
|
|
case uint32:
|
|
return strconv.FormatUint(uint64(k), 10), nil
|
|
case uint64:
|
|
return strconv.FormatUint(k, 10), nil
|
|
case int:
|
|
return strconv.Itoa(k), nil
|
|
case int8:
|
|
return strconv.FormatInt(int64(k), 10), nil
|
|
case int16:
|
|
return strconv.FormatInt(int64(k), 10), nil
|
|
case int32:
|
|
return strconv.FormatInt(int64(k), 10), nil
|
|
case int64:
|
|
return strconv.FormatInt(k, 10), nil
|
|
default:
|
|
return "", fmt.Errorf("unexpected map key type, got: %T: %w", k, ErrYAML)
|
|
}
|
|
}
|
|
|
|
func transformData(input any) (out any, err error) {
|
|
switch in := input.(type) {
|
|
case yaml.Node:
|
|
return yamlNode(&in)
|
|
case *yaml.Node:
|
|
return yamlNode(in)
|
|
case map[any]any:
|
|
o := make(YAMLMapSlice, 0, len(in))
|
|
for ke, va := range in {
|
|
var nmi YAMLMapItem
|
|
if nmi.Key, err = format(ke); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
v, ert := transformData(va)
|
|
if ert != nil {
|
|
return nil, ert
|
|
}
|
|
nmi.Value = v
|
|
o = append(o, nmi)
|
|
}
|
|
return o, nil
|
|
case []any:
|
|
len1 := len(in)
|
|
o := make([]any, len1)
|
|
for i := range len1 {
|
|
o[i], err = transformData(in[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return o, nil
|
|
}
|
|
return input, nil
|
|
}
|