wstl1/mapping_engine/util/jsonutil/jsonmetautil.go (272 lines of code) (raw):
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jsonutil
import (
"fmt"
"strconv"
"strings"
)
// JSONMetaNode represents a JSONToken with at least a Parent and Key, and some way to derive a full
// JSON dot/bracket notation path.
type JSONMetaNode interface {
Parent() JSONMetaNode
Key() string
Path() string
ContentString(level int) string
Provenance() Provenance
ProvenanceString() string
}
// Provenance contains information on the source provenance of this node if it was created
// by the computation of some other nodes going through a function.
type Provenance struct {
Sources []JSONMetaNode
Function string
}
// ShallowString returns the paths of the arguments and the function used to get this node. It does
// not recursively check for provenance of the parent nodes.
func (c Provenance) ShallowString() string {
parents := []string{}
for _, s := range c.Sources {
str := "..."
if s.Path() != s.Key() && s.Key() != "" {
str = s.Path()
} else if s.Key() != "" {
str = "..." + s.Key()
} else if s.Provenance().Function != "" {
str = fmt.Sprintf("%s(...)", s.Provenance().Function)
}
parents = append(parents, str)
}
if c.Function != "" {
return fmt.Sprintf("%s( %s )", c.Function, strings.Join(parents, ", "))
}
return strings.Join(parents, ", ")
}
// JSONMeta is a container for the common JSONMeta data.
type JSONMeta struct {
key string
provenance Provenance
}
// NewJSONMeta creates a new JSONMeta with the given key and Provenance information.
func NewJSONMeta(key string, cp Provenance) JSONMeta {
return JSONMeta{
key: key,
provenance: cp,
}
}
// Key returns this Meta's key (boxed array index like [1] for array elements,
// dotted keys like ".foo" for container fields).
func (jm JSONMeta) Key() string {
return jm.key
}
// Parent returns this Meta's parent node, if this node was not computationally derived.
func (jm JSONMeta) Parent() JSONMetaNode {
if len(jm.provenance.Sources) != 1 || jm.provenance.Function != "" {
return nil
}
return jm.provenance.Sources[0]
}
// Provenance returns the provenance information of this node.
func (jm JSONMeta) Provenance() Provenance {
return jm.provenance
}
// ContentString prints the meta content as a string.
func (jm JSONMeta) ContentString(_ int) string {
return fmt.Sprintf("%s => %v", jm.Path(), jm.Parent())
}
// Path returns the full JSON path in dot/bracket notation to this Meta by following its parents.
func (jm JSONMeta) Path() string {
sb := make([]string, 0)
var c JSONMetaNode = jm
for c != nil {
sb = append([]string{c.Key()}, sb...)
// Prepend a "." for non-index segments.
if _, ok := c.Parent().(JSONMetaContainerNode); ok {
sb = append([]string{"."}, sb...)
}
c = c.Parent()
}
return strings.TrimLeft(strings.Join(sb, ""), ".")
}
// ProvenanceString produces a string representing the approximate provenance of this JSONMeta. This
// uses Provenance.ShallowString and thus may contain incomplete information. It shall not
// include any content of the data.
func (jm JSONMeta) ProvenanceString() string {
if jm.Key() == "" {
return jm.provenance.ShallowString()
}
cp := jm.Provenance().ShallowString()
if cp == "" {
return jm.Key()
}
return fmt.Sprintf("%s.%s", cp, jm.Key())
}
// JSONMetaContainerNode is a JSONMetaNode with an additional Children value.
type JSONMetaContainerNode struct {
// Value used intentionally to avoid modification.
JSONMeta
Children map[string]JSONMetaNode
}
// String produces a string representation of JSONMetaContainerNode, including its path.
func (j JSONMetaContainerNode) String() string {
return fmt.Sprintf("(%q -> %s)", j.Path(), j.ContentString(0))
}
// ContentString produces a string representation of the contents of JSONMetaContainerNode.
func (j JSONMetaContainerNode) ContentString(level int) string {
if level > 1 {
return "{...}"
}
var o []string
for k, v := range j.Children {
vs := "null"
if v != nil {
vs = v.ContentString(level + 1)
}
o = append(o, fmt.Sprintf("%s:%v", k, vs))
}
return fmt.Sprintf("{%s}", strings.Join(o, ", "))
}
// JSONMetaArrayNode is a JSONMetaNode with an additional Array value.
type JSONMetaArrayNode struct {
// Value used intentionally to avoid modification.
JSONMeta
Items []JSONMetaNode
}
// String produces a string representation of JSONMetaArrayNode, including its path.
func (j JSONMetaArrayNode) String() string {
return fmt.Sprintf("(%q -> %s)", j.Path(), j.ContentString(0))
}
// ContentString produces a string representation of the contents of JSONMetaArrayNode.
func (j JSONMetaArrayNode) ContentString(level int) string {
if level > 1 {
return "[...]"
}
var o []string
for _, v := range j.Items {
vs := "null"
if v != nil {
vs = v.ContentString(level + 1)
}
o = append(o, vs)
}
return fmt.Sprintf("[%s]", strings.Join(o, ", "))
}
// JSONMetaPrimitiveNode is a JSONMetaNode with an additional primitive value.
type JSONMetaPrimitiveNode struct {
// Value used intentionally to avoid modification.
JSONMeta
Value JSONPrimitive
}
// String produces a string representation of JSONMetaPrimitiveNode, including its path.
func (j JSONMetaPrimitiveNode) String() string {
return fmt.Sprintf("(%q -> %s)", j.Path(), j.ContentString(0))
}
// ContentString produces a string representation of the contents of JSONMetaPrimitiveNode.
func (j JSONMetaPrimitiveNode) ContentString(_ int) string {
return fmt.Sprintf("%v", j.Value)
}
// TokenToNode converts a JSONToken to a JSONMetaNode, filling the meta data.
func TokenToNode(token JSONToken) (JSONMetaNode, error) {
return tokenToNode(Provenance{}, "", token)
}
// TokenToNodeWithProvenance converts a JSONToken to a JSONMetaNode, filling the meta data with the
// given provenance.
func TokenToNodeWithProvenance(token JSONToken, key string, cp Provenance) (JSONMetaNode, error) {
return tokenToNode(cp, key, token)
}
func tokenToNode(cp Provenance, key string, token JSONToken) (JSONMetaNode, error) {
m := JSONMeta{key: key, provenance: cp}
switch t := token.(type) {
case JSONContainer:
cn := JSONMetaContainerNode{JSONMeta: m, Children: make(map[string]JSONMetaNode)}
for k, v := range t {
vn, err := tokenToNode(Provenance{Sources: []JSONMetaNode{cn}}, k, *v)
if err != nil {
return nil, err
}
cn.Children[k] = vn
}
return cn, nil
case JSONArr:
an := JSONMetaArrayNode{JSONMeta: m, Items: make([]JSONMetaNode, 0, len(t))}
for i, v := range t {
// "[i]" is the key.
vn, err := tokenToNode(Provenance{Sources: []JSONMetaNode{an}}, "["+strconv.Itoa(i)+"]", v)
if err != nil {
return nil, err
}
an.Items = append(an.Items, vn)
}
return an, nil
case JSONStr, JSONNum, JSONBool:
return JSONMetaPrimitiveNode{JSONMeta: m, Value: t.(JSONPrimitive)}, nil
case nil:
return nil, nil
default:
return nil, fmt.Errorf("unexpected token type: %T", t)
}
}
// NodeToToken converts a JSONMetaNode to a JSONToken, losing the meta data.
func NodeToToken(node JSONMetaNode) (JSONToken, error) {
switch n := node.(type) {
case JSONMetaContainerNode:
jc := make(JSONContainer)
for k, v := range n.Children {
t, err := NodeToToken(v)
if err != nil {
return nil, err
}
jc[k] = &t
}
return jc, nil
case JSONMetaArrayNode:
ja := make(JSONArr, 0, len(n.Items))
for _, v := range n.Items {
t, err := NodeToToken(v)
if err != nil {
return nil, err
}
ja = append(ja, t)
}
return ja, nil
case JSONMetaPrimitiveNode:
return n.Value.(JSONToken), nil
case nil:
return nil, nil
default:
return nil, fmt.Errorf("unexpected node type: %T", n)
}
}
// GetNodeField returns the child given a path in dot/bracket notation like foo.bar[3].baz
func GetNodeField(node JSONMetaNode, path string) (JSONMetaNode, error) {
segs, err := SegmentPath(path)
if err != nil {
return nil, fmt.Errorf("failed to segment path: %v", err)
}
return GetNodeFieldSegmented(node, segs)
}
// GetNodeFieldSegmented returns the child given a path in parsed dot/bracket notation like
// ["foo", "bar", "[3]", "baz"]
func GetNodeFieldSegmented(node JSONMetaNode, segments []string) (JSONMetaNode, error) {
n, _, err := getNodeFieldSegmented(node, segments)
return n, err
}
// getNodeFieldSegmented finds the node given a segmented path. It returns the node (if found) and
// a boolean indicating whether an array expansion [*] occurred somewhere in the given path.
func getNodeFieldSegmented(node JSONMetaNode, segments []string) (JSONMetaNode, bool, error) {
if len(segments) == 0 {
return node, false, nil
}
seg := segments[0]
switch n := node.(type) {
case JSONMetaPrimitiveNode:
return nil, false, fmt.Errorf("attempted to key into primitive with %q", seg)
case JSONMetaArrayNode:
if !IsIndex(seg) {
return nil, false, fmt.Errorf("expected an array index with brackets like [123] or [*] but got %q", seg)
}
idxSubstr := seg[1 : len(seg)-1]
if idxSubstr == "*" {
retNode := JSONMetaArrayNode{
JSONMeta: n.JSONMeta,
}
for i := range n.Items {
f, expand, err := getNodeFieldSegmented(n.Items[i], segments[1:])
if err != nil {
return nil, false, fmt.Errorf("error expanding [*] on item index %d: %v", i, err)
}
// If an array expansion occurs down the line, we need to unnest the resulting array here.
if expand {
fArr, ok := f.(JSONMetaArrayNode)
if !ok {
return nil, false, fmt.Errorf("bug: getNodeFieldSegmented returned true for expansion but value was not an array (was %T)", f)
}
retNode.Items = append(retNode.Items, fArr.Items...)
} else {
retNode.Items = append(retNode.Items, f)
}
}
return retNode, true, nil
}
idx, err := strconv.Atoi(idxSubstr)
if err != nil {
return nil, false, fmt.Errorf("could not parse array index %q: %v", seg, err)
}
if idx < 0 {
return nil, false, fmt.Errorf("negative array indices are not supported but got %d", idx)
}
if idx >= len(n.Items) {
// TODO(): Consider returning a different value for fields that don't exist vs
// fields that are actually set to null.
return nil, false, nil
}
return getNodeFieldSegmented(n.Items[idx], segments[1:])
case JSONMetaContainerNode:
if IsIndex(seg) {
return nil, false, fmt.Errorf("expected an object key, but got an array index %q", seg)
}
if val, ok := n.Children[seg]; ok {
return getNodeFieldSegmented(val, segments[1:])
}
// TODO(): Consider returning a different value for fields that don't exist vs
// fields that are actually set to null.
return nil, false, nil
case nil:
return nil, false, nil
default:
return nil, false, fmt.Errorf("found node of un-navigable type %T", node)
}
}