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 }