// Copyright (c) 2016 Arista Networks, Inc. // Use of this source code is governed by the Apache License 2.0 // that can be found in the COPYING file. package openconfig import ( "bytes" "encoding/json" "fmt" "strings" "github.com/openconfig/reference/rpc/openconfig" ) // joinPath builds a string out of an Element func joinPath(path *openconfig.Path) string { if path == nil { return "" } return strings.Join(path.Element, "/") } func convertUpdate(update *openconfig.Update) (interface{}, error) { switch update.Value.Type { case openconfig.Type_JSON: var value interface{} decoder := json.NewDecoder(bytes.NewReader(update.Value.Value)) decoder.UseNumber() if err := decoder.Decode(&value); err != nil { return nil, fmt.Errorf("Malformed JSON update %q in %s", update.Value.Value, update) } return value, nil case openconfig.Type_BYTES: return update.Value.Value, nil default: return nil, fmt.Errorf("Unhandled type of value %v in %s", update.Value.Type, update) } } // NotificationToJSON converts a Notification into a JSON string func NotificationToJSON(notif *openconfig.Notification) (string, error) { m := make(map[string]interface{}, 1) m["timestamp"] = notif.Timestamp m["path"] = "/" + joinPath(notif.Prefix) if len(notif.Update) != 0 { updates := make(map[string]interface{}, len(notif.Update)) var err error for _, update := range notif.Update { updates[joinPath(update.Path)], err = convertUpdate(update) if err != nil { return "", err } } m["updates"] = updates } if len(notif.Delete) != 0 { deletes := make([]string, len(notif.Delete)) for i, del := range notif.Delete { deletes[i] = joinPath(del) } m["deletes"] = deletes } m = map[string]interface{}{"notification": m} js, err := json.MarshalIndent(m, "", " ") if err != nil { return "", err } return string(js), nil } // SubscribeResponseToJSON converts a SubscribeResponse into a JSON string func SubscribeResponseToJSON(resp *openconfig.SubscribeResponse) (string, error) { m := make(map[string]interface{}, 1) var err error switch resp := resp.Response.(type) { case *openconfig.SubscribeResponse_Update: return NotificationToJSON(resp.Update) case *openconfig.SubscribeResponse_Heartbeat: m["heartbeat"] = resp.Heartbeat.Interval case *openconfig.SubscribeResponse_SyncResponse: m["syncResponse"] = resp.SyncResponse default: return "", fmt.Errorf("Unknown type of response: %T: %s", resp, resp) } js, err := json.MarshalIndent(m, "", " ") if err != nil { return "", err } return string(js), nil } // EscapeFunc is the escaping method for attribute names type EscapeFunc func(k string) string // escapeValue looks for maps in an interface and escapes their keys func escapeValue(value interface{}, escape EscapeFunc) interface{} { valueMap, ok := value.(map[string]interface{}) if !ok { return value } escapedMap := make(map[string]interface{}, len(valueMap)) for k, v := range valueMap { escapedKey := escape(k) escapedMap[escapedKey] = escapeValue(v, escape) } return escapedMap } // addPathToMap creates a map[string]interface{} from a path. It returns the node in // the map corresponding to the last element in the path func addPathToMap(root map[string]interface{}, path []string, escape EscapeFunc) ( map[string]interface{}, error) { parent := root for _, element := range path { k := escape(element) node, found := parent[k] if !found { node = map[string]interface{}{} parent[k] = node } var ok bool parent, ok = node.(map[string]interface{}) if !ok { return nil, fmt.Errorf( "Node %s is of type %T (expected map[string]interface traversing %q)", element, node, path) } } return parent, nil } // NotificationToMap maps a Notification into a nested map of entities func NotificationToMap(addr string, notification *openconfig.Notification, escape EscapeFunc) (map[string]interface{}, error) { if escape == nil { escape = func(name string) string { return name } } prefix := notification.GetPrefix() // Convert deletes var deletes map[string]interface{} notificationDeletes := notification.GetDelete() if notificationDeletes != nil { deletes = make(map[string]interface{}) node := deletes if prefix != nil { var err error node, err = addPathToMap(node, prefix.Element, escape) if err != nil { return nil, err } } for _, delete := range notificationDeletes { _, err := addPathToMap(node, delete.Element, escape) if err != nil { return nil, err } } } // Convert updates var updates map[string]interface{} notificationUpdates := notification.GetUpdate() if notificationUpdates != nil { updates = make(map[string]interface{}) node := updates if prefix != nil { var err error node, err = addPathToMap(node, prefix.Element, escape) if err != nil { return nil, err } } for _, update := range notificationUpdates { updateNode := node path := update.GetPath() elementLen := len(path.Element) // Convert all elements before the leaf if elementLen > 1 { parentElements := path.Element[:elementLen-1] var err error updateNode, err = addPathToMap(updateNode, parentElements, escape) if err != nil { return nil, err } } // Convert the value in the leaf value := update.GetValue() var unmarshaledValue interface{} switch value.Type { case openconfig.Type_JSON: if err := json.Unmarshal(value.Value, &unmarshaledValue); err != nil { return nil, err } case openconfig.Type_BYTES: unmarshaledValue = update.Value.Value default: return nil, fmt.Errorf("Unexpected value type %s for path %v", value.Type, path) } updateNode[escape(path.Element[elementLen-1])] = escapeValue( unmarshaledValue, escape) } } // Build the complete map to return root := map[string]interface{}{ "timestamp": notification.Timestamp, } if addr != "" { root["dataset"] = addr } if deletes != nil { root["delete"] = deletes } if updates != nil { root["update"] = updates } return root, nil } // NotificationToJSONDocument maps a Notification into a single JSON document func NotificationToJSONDocument(addr string, notification *openconfig.Notification, escape EscapeFunc) ([]byte, error) { m, err := NotificationToMap(addr, notification, escape) if err != nil { return nil, err } return json.Marshal(m) }