github.com/aristanetworks/goarista@v0.0.0-20240514173732-cca2755bbd44/openconfig/json.go (about)

     1  // Copyright (c) 2016 Arista Networks, Inc.
     2  // Use of this source code is governed by the Apache License 2.0
     3  // that can be found in the COPYING file.
     4  
     5  package openconfig
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/json"
    10  	"fmt"
    11  	"strings"
    12  
    13  	"github.com/openconfig/gnmi/proto/gnmi"
    14  )
    15  
    16  // joinPath builds a string out of an Element
    17  func joinPath(path *gnmi.Path) string {
    18  	if path == nil {
    19  		return ""
    20  	}
    21  	return strings.Join(path.Element, "/")
    22  }
    23  
    24  func convertUpdate(update *gnmi.Update) (interface{}, error) {
    25  	switch update.Value.Type {
    26  	case gnmi.Encoding_JSON:
    27  		var value interface{}
    28  		decoder := json.NewDecoder(bytes.NewReader(update.Value.Value))
    29  		decoder.UseNumber()
    30  		if err := decoder.Decode(&value); err != nil {
    31  			return nil, fmt.Errorf("Malformed JSON update %q in %s",
    32  				update.Value.Value, update)
    33  		}
    34  		return value, nil
    35  	case gnmi.Encoding_BYTES:
    36  		return update.Value.Value, nil
    37  	default:
    38  		return nil,
    39  			fmt.Errorf("Unhandled type of value %v in %s", update.Value.Type, update)
    40  	}
    41  }
    42  
    43  // NotificationToJSON converts a Notification into a JSON string
    44  func NotificationToJSON(notif *gnmi.Notification) (string, error) {
    45  	m := make(map[string]interface{}, 1)
    46  	m["timestamp"] = notif.Timestamp
    47  	m["path"] = "/" + joinPath(notif.Prefix)
    48  	if len(notif.Update) != 0 {
    49  		updates := make(map[string]interface{}, len(notif.Update))
    50  		var err error
    51  		for _, update := range notif.Update {
    52  			updates[joinPath(update.Path)], err = convertUpdate(update)
    53  			if err != nil {
    54  				return "", err
    55  			}
    56  		}
    57  		m["updates"] = updates
    58  	}
    59  	if len(notif.Delete) != 0 {
    60  		deletes := make([]string, len(notif.Delete))
    61  		for i, del := range notif.Delete {
    62  			deletes[i] = joinPath(del)
    63  		}
    64  		m["deletes"] = deletes
    65  	}
    66  	m = map[string]interface{}{"notification": m}
    67  	js, err := json.MarshalIndent(m, "", "  ")
    68  	if err != nil {
    69  		return "", err
    70  	}
    71  	return string(js), nil
    72  }
    73  
    74  // SubscribeResponseToJSON converts a SubscribeResponse into a JSON string
    75  func SubscribeResponseToJSON(resp *gnmi.SubscribeResponse) (string, error) {
    76  	m := make(map[string]interface{}, 1)
    77  	var err error
    78  	switch resp := resp.Response.(type) {
    79  	case *gnmi.SubscribeResponse_Update:
    80  		return NotificationToJSON(resp.Update)
    81  	case *gnmi.SubscribeResponse_SyncResponse:
    82  		m["syncResponse"] = resp.SyncResponse
    83  	default:
    84  		return "", fmt.Errorf("Unknown type of response: %T: %s", resp, resp)
    85  	}
    86  	js, err := json.MarshalIndent(m, "", "  ")
    87  	if err != nil {
    88  		return "", err
    89  	}
    90  	return string(js), nil
    91  }
    92  
    93  // EscapeFunc is the escaping method for attribute names
    94  type EscapeFunc func(k string) string
    95  
    96  // escapeValue looks for maps in an interface and escapes their keys
    97  func escapeValue(value interface{}, escape EscapeFunc) interface{} {
    98  	valueMap, ok := value.(map[string]interface{})
    99  	if !ok {
   100  		return value
   101  	}
   102  	escapedMap := make(map[string]interface{}, len(valueMap))
   103  	for k, v := range valueMap {
   104  		escapedKey := escape(k)
   105  		escapedMap[escapedKey] = escapeValue(v, escape)
   106  	}
   107  	return escapedMap
   108  }
   109  
   110  // addPathToMap creates a map[string]interface{} from a path. It returns the node in
   111  // the map corresponding to the last element in the path
   112  func addPathToMap(root map[string]interface{}, path []string, escape EscapeFunc) (
   113  	map[string]interface{}, error) {
   114  	parent := root
   115  	for _, element := range path {
   116  		k := escape(element)
   117  		node, found := parent[k]
   118  		if !found {
   119  			node = map[string]interface{}{}
   120  			parent[k] = node
   121  		}
   122  		var ok bool
   123  		parent, ok = node.(map[string]interface{})
   124  		if !ok {
   125  			return nil, fmt.Errorf(
   126  				"Node %s is of type %T (expected map[string]interface traversing %q)",
   127  				element, node, path)
   128  		}
   129  	}
   130  	return parent, nil
   131  }
   132  
   133  // NotificationToMap maps a Notification into a nested map of entities
   134  func NotificationToMap(addr string, notification *gnmi.Notification,
   135  	escape EscapeFunc) (map[string]interface{}, error) {
   136  	if escape == nil {
   137  		escape = func(name string) string {
   138  			return name
   139  		}
   140  	}
   141  	prefix := notification.GetPrefix()
   142  
   143  	// Convert deletes
   144  	var deletes map[string]interface{}
   145  	notificationDeletes := notification.GetDelete()
   146  	if notificationDeletes != nil {
   147  		deletes = make(map[string]interface{})
   148  		node := deletes
   149  		if prefix != nil {
   150  			var err error
   151  			node, err = addPathToMap(node, prefix.Element, escape)
   152  			if err != nil {
   153  				return nil, err
   154  			}
   155  		}
   156  		for _, delete := range notificationDeletes {
   157  			_, err := addPathToMap(node, delete.Element, escape)
   158  			if err != nil {
   159  				return nil, err
   160  			}
   161  		}
   162  	}
   163  
   164  	// Convert updates
   165  	var updates map[string]interface{}
   166  	notificationUpdates := notification.GetUpdate()
   167  	if notificationUpdates != nil {
   168  		updates = make(map[string]interface{})
   169  		node := updates
   170  		if prefix != nil {
   171  			var err error
   172  			node, err = addPathToMap(node, prefix.Element, escape)
   173  			if err != nil {
   174  				return nil, err
   175  			}
   176  		}
   177  		for _, update := range notificationUpdates {
   178  			updateNode := node
   179  			path := update.GetPath()
   180  			elementLen := len(path.Element)
   181  
   182  			// Convert all elements before the leaf
   183  			if elementLen > 1 {
   184  				parentElements := path.Element[:elementLen-1]
   185  				var err error
   186  				updateNode, err = addPathToMap(updateNode, parentElements, escape)
   187  				if err != nil {
   188  					return nil, err
   189  				}
   190  			}
   191  
   192  			// Convert the value in the leaf
   193  			value := update.GetValue()
   194  			var unmarshaledValue interface{}
   195  			switch value.Type {
   196  			case gnmi.Encoding_JSON:
   197  				if err := json.Unmarshal(value.Value, &unmarshaledValue); err != nil {
   198  					return nil, err
   199  				}
   200  			case gnmi.Encoding_BYTES:
   201  				unmarshaledValue = update.Value.Value
   202  			default:
   203  				return nil, fmt.Errorf("Unexpected value type %s for path %v",
   204  					value.Type, path)
   205  			}
   206  			updateNode[escape(path.Element[elementLen-1])] = escapeValue(
   207  				unmarshaledValue, escape)
   208  		}
   209  	}
   210  
   211  	// Build the complete map to return
   212  	root := map[string]interface{}{
   213  		"timestamp": notification.Timestamp,
   214  	}
   215  	if addr != "" {
   216  		root["dataset"] = addr
   217  	}
   218  	if deletes != nil {
   219  		root["delete"] = deletes
   220  	}
   221  	if updates != nil {
   222  		root["update"] = updates
   223  	}
   224  	return root, nil
   225  }
   226  
   227  // NotificationToJSONDocument maps a Notification into a single JSON document
   228  func NotificationToJSONDocument(addr string, notification *gnmi.Notification,
   229  	escape EscapeFunc) ([]byte, error) {
   230  	m, err := NotificationToMap(addr, notification, escape)
   231  	if err != nil {
   232  		return nil, err
   233  	}
   234  	return json.Marshal(m)
   235  }