cli-utils/pkg/jsonpath/jsonpath.go

189 lines
5.3 KiB
Go

// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package jsonpath
import (
"encoding/json"
"fmt"
// Using gopkg.in/yaml.v3 instead of sigs.k8s.io/yaml on purpose.
// yaml.v3 correctly parses ints:
// https://github.com/kubernetes-sigs/yaml/issues/45
// yaml.v3 Node is also used as input to yqlib.
"gopkg.in/yaml.v3"
"k8s.io/klog/v2"
"github.com/spyzhov/ajson"
)
// Get evaluates the JSONPath expression to extract values from the input map.
// Returns the node values that were found (zero or more), or an error.
// For details about the JSONPath expression language, see:
// https://goessner.net/articles/JsonPath/
func Get(obj map[string]interface{}, expression string) ([]interface{}, error) {
// format input object as json for input into jsonpath library
jsonBytes, err := json.Marshal(obj)
if err != nil {
return nil, fmt.Errorf("failed to marshal input to json: %w", err)
}
klog.V(7).Infof("jsonpath.Get input as json:\n%s", jsonBytes)
// parse json into an ajson node
root, err := ajson.Unmarshal(jsonBytes)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal input json: %w", err)
}
// find nodes that match the expression
nodes, err := root.JSONPath(expression)
if err != nil {
return nil, fmt.Errorf("failed to evaluate jsonpath expression (%s): %w", expression, err)
}
result := make([]interface{}, len(nodes))
// get value of all matching nodes
for i, node := range nodes {
// format node value as json
jsonBytes, err = ajson.Marshal(node)
if err != nil {
return nil, fmt.Errorf("failed to marshal jsonpath result to json: %w", err)
}
klog.V(7).Infof("jsonpath.Get output as json:\n%s", jsonBytes)
// parse json back into a Go primitive
var value interface{}
err = yaml.Unmarshal(jsonBytes, &value)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal jsonpath result: %w", err)
}
result[i] = value
}
return result, nil
}
// Set evaluates the JSONPath expression to set a value in the input map.
// Returns the number of matching nodes that were updated, or an error.
// For details about the JSONPath expression language, see:
// https://goessner.net/articles/JsonPath/
func Set(obj map[string]interface{}, expression string, value interface{}) (int, error) {
// format input object as json for input into jsonpath library
jsonBytes, err := json.Marshal(obj)
if err != nil {
return 0, fmt.Errorf("failed to marshal input to json: %w", err)
}
klog.V(7).Infof("jsonpath.Set input as json:\n%s", jsonBytes)
// parse json into an ajson node
root, err := ajson.Unmarshal(jsonBytes)
if err != nil {
return 0, fmt.Errorf("failed to unmarshal input json: %w", err)
}
// retrieve nodes that match the expression
nodes, err := root.JSONPath(expression)
if err != nil {
return 0, fmt.Errorf("failed to evaluate jsonpath expression (%s): %w", expression, err)
}
if len(nodes) == 0 {
// zero nodes found, none updated
return 0, nil
}
// set value of all matching nodes
for _, node := range nodes {
switch typedValue := value.(type) {
case bool:
err = node.SetBool(typedValue)
case string:
err = node.SetString(typedValue)
case int:
err = node.SetNumeric(float64(typedValue))
case float64:
err = node.SetNumeric(typedValue)
case []interface{}:
var arrayValue []*ajson.Node
arrayValue, err = toArrayOfNodes(typedValue)
if err != nil {
break
}
err = node.SetArray(arrayValue)
case map[string]interface{}:
var mapValue map[string]*ajson.Node
mapValue, err = toMapOfNodes(typedValue)
if err != nil {
break
}
err = node.SetObject(mapValue)
default:
if value == nil {
err = node.SetNull()
} else {
err = fmt.Errorf("unsupported value type: %T", value)
}
}
if err != nil {
return 0, err
}
}
// format into an ajson node
jsonBytes, err = ajson.Marshal(root)
if err != nil {
return 0, fmt.Errorf("failed to marshal jsonpath result to json: %w", err)
}
klog.V(7).Infof("jsonpath.Set output as json:\n%s", jsonBytes)
// parse json back into the input map
err = yaml.Unmarshal(jsonBytes, &obj)
if err != nil {
return 0, fmt.Errorf("failed to unmarshal jsonpath result: %w", err)
}
return len(nodes), nil
}
func toArrayOfNodes(obj []interface{}) ([]*ajson.Node, error) {
out := make([]*ajson.Node, len(obj))
for index, value := range obj {
// format input object as json for input into jsonpath library
jsonBytes, err := json.Marshal(value)
if err != nil {
return nil, fmt.Errorf("failed to marshal array element to json: %w", err)
}
// parse json into an ajson node
node, err := ajson.Unmarshal(jsonBytes)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal array element: %w", err)
}
out[index] = node
}
return out, nil
}
func toMapOfNodes(obj map[string]interface{}) (map[string]*ajson.Node, error) {
out := make(map[string]*ajson.Node, len(obj))
for key, value := range obj {
// format input object as json for input into jsonpath library
jsonBytes, err := json.Marshal(value)
if err != nil {
return nil, fmt.Errorf("failed to marshal map value to json: %w", err)
}
// parse json into an ajson node
node, err := ajson.Unmarshal(jsonBytes)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal map value: %w", err)
}
out[key] = node
}
return out, nil
}