David K. Bainbridge | 528b318 | 2017-01-23 08:51:59 -0800 | [diff] [blame] | 1 | // Copyright 2012-2016 Canonical Ltd. |
| 2 | // Licensed under the LGPLv3, see LICENCE file for details. |
| 3 | |
| 4 | package gomaasapi |
| 5 | |
| 6 | import ( |
| 7 | "encoding/json" |
| 8 | "errors" |
| 9 | "fmt" |
| 10 | ) |
| 11 | |
| 12 | // JSONObject is a wrapper around a JSON structure which provides |
| 13 | // methods to extract data from that structure. |
| 14 | // A JSONObject provides a simple structure consisting of the data types |
| 15 | // defined in JSON: string, number, object, list, and bool. To get the |
| 16 | // value you want out of a JSONObject, you must know (or figure out) which |
| 17 | // kind of value you have, and then call the appropriate Get*() method to |
| 18 | // get at it. Reading an item as the wrong type will return an error. |
| 19 | // For instance, if your JSONObject consists of a number, call GetFloat64() |
| 20 | // to get the value as a float64. If it's a list, call GetArray() to get |
| 21 | // a slice of JSONObjects. To read any given item from the slice, you'll |
| 22 | // need to "Get" that as the right type as well. |
| 23 | // There is one exception: a MAASObject is really a special kind of map, |
| 24 | // so you can read it as either. |
| 25 | // Reading a null item is also an error. So before you try obj.Get*(), |
| 26 | // first check obj.IsNil(). |
| 27 | type JSONObject struct { |
| 28 | // Parsed value. May actually be any of the types a JSONObject can |
| 29 | // wrap, except raw bytes. If the object can only be interpreted |
| 30 | // as raw bytes, this will be nil. |
| 31 | value interface{} |
| 32 | // Raw bytes, if this object was parsed directly from an API response. |
| 33 | // Is nil for sub-objects found within other objects. An object that |
| 34 | // was parsed directly from a response can be both raw bytes and some |
| 35 | // other value at the same time. |
| 36 | // For example, "[]" looks like a JSON list, so you can read it as an |
| 37 | // array. But it may also be the raw contents of a file that just |
| 38 | // happens to look like JSON, and so you can read it as raw bytes as |
| 39 | // well. |
| 40 | bytes []byte |
| 41 | // Client for further communication with the API. |
| 42 | client Client |
| 43 | // Is this a JSON null? |
| 44 | isNull bool |
| 45 | } |
| 46 | |
| 47 | // Our JSON processor distinguishes a MAASObject from a jsonMap by the fact |
| 48 | // that it contains a key "resource_uri". (A regular map might contain the |
| 49 | // same key through sheer coincide, but never mind: you can still treat it |
| 50 | // as a jsonMap and never notice the difference.) |
| 51 | const resourceURI = "resource_uri" |
| 52 | |
| 53 | // maasify turns a completely untyped json.Unmarshal result into a JSONObject |
| 54 | // (with the appropriate implementation of course). This function is |
| 55 | // recursive. Maps and arrays are deep-copied, with each individual value |
| 56 | // being converted to a JSONObject type. |
| 57 | func maasify(client Client, value interface{}) JSONObject { |
| 58 | if value == nil { |
| 59 | return JSONObject{isNull: true} |
| 60 | } |
| 61 | switch value.(type) { |
| 62 | case string, float64, bool: |
| 63 | return JSONObject{value: value} |
| 64 | case map[string]interface{}: |
| 65 | original := value.(map[string]interface{}) |
| 66 | result := make(map[string]JSONObject, len(original)) |
| 67 | for key, value := range original { |
| 68 | result[key] = maasify(client, value) |
| 69 | } |
| 70 | return JSONObject{value: result, client: client} |
| 71 | case []interface{}: |
| 72 | original := value.([]interface{}) |
| 73 | result := make([]JSONObject, len(original)) |
| 74 | for index, value := range original { |
| 75 | result[index] = maasify(client, value) |
| 76 | } |
| 77 | return JSONObject{value: result} |
| 78 | } |
| 79 | msg := fmt.Sprintf("Unknown JSON type, can't be converted to JSONObject: %v", value) |
| 80 | panic(msg) |
| 81 | } |
| 82 | |
| 83 | // Parse a JSON blob into a JSONObject. |
| 84 | func Parse(client Client, input []byte) (JSONObject, error) { |
| 85 | var obj JSONObject |
| 86 | if input == nil { |
| 87 | panic(errors.New("Parse() called with nil input")) |
| 88 | } |
| 89 | var parsed interface{} |
| 90 | err := json.Unmarshal(input, &parsed) |
| 91 | if err == nil { |
| 92 | obj = maasify(client, parsed) |
| 93 | obj.bytes = input |
| 94 | } else { |
| 95 | switch err.(type) { |
| 96 | case *json.InvalidUTF8Error: |
| 97 | case *json.SyntaxError: |
| 98 | // This isn't JSON. Treat it as raw binary data. |
| 99 | default: |
| 100 | return obj, err |
| 101 | } |
| 102 | obj = JSONObject{value: nil, client: client, bytes: input} |
| 103 | } |
| 104 | return obj, nil |
| 105 | } |
| 106 | |
| 107 | // JSONObjectFromStruct takes a struct and converts it to a JSONObject |
| 108 | func JSONObjectFromStruct(client Client, input interface{}) (JSONObject, error) { |
| 109 | j, err := json.MarshalIndent(input, "", " ") |
| 110 | if err != nil { |
| 111 | return JSONObject{}, err |
| 112 | } |
| 113 | return Parse(client, j) |
| 114 | } |
| 115 | |
| 116 | // Return error value for failed type conversion. |
| 117 | func failConversion(wantedType string, obj JSONObject) error { |
| 118 | msg := fmt.Sprintf("Requested %v, got %T.", wantedType, obj.value) |
| 119 | return errors.New(msg) |
| 120 | } |
| 121 | |
| 122 | // MarshalJSON tells the standard json package how to serialize a JSONObject |
| 123 | // back to JSON. |
| 124 | func (obj JSONObject) MarshalJSON() ([]byte, error) { |
| 125 | if obj.IsNil() { |
| 126 | return json.Marshal(nil) |
| 127 | } |
| 128 | return json.MarshalIndent(obj.value, "", " ") |
| 129 | } |
| 130 | |
| 131 | // With MarshalJSON, JSONObject implements json.Marshaler. |
| 132 | var _ json.Marshaler = (*JSONObject)(nil) |
| 133 | |
| 134 | // IsNil tells you whether a JSONObject is a JSON "null." |
| 135 | // There is one irregularity. If the original JSON blob was actually raw |
| 136 | // data, not JSON, then its IsNil will return false because the object |
| 137 | // contains the binary data as a non-nil value. But, if the original JSON |
| 138 | // blob consisted of a null, then IsNil returns true even though you can |
| 139 | // still retrieve binary data from it. |
| 140 | func (obj JSONObject) IsNil() bool { |
| 141 | if obj.value != nil { |
| 142 | return false |
| 143 | } |
| 144 | if obj.bytes == nil { |
| 145 | return true |
| 146 | } |
| 147 | // This may be a JSON null. We can't expect every JSON null to look |
| 148 | // the same; there may be leading or trailing space. |
| 149 | return obj.isNull |
| 150 | } |
| 151 | |
| 152 | // GetString retrieves the object's value as a string. If the value wasn't |
| 153 | // a JSON string, that's an error. |
| 154 | func (obj JSONObject) GetString() (value string, err error) { |
| 155 | value, ok := obj.value.(string) |
| 156 | if !ok { |
| 157 | err = failConversion("string", obj) |
| 158 | } |
| 159 | return |
| 160 | } |
| 161 | |
| 162 | // GetFloat64 retrieves the object's value as a float64. If the value wasn't |
| 163 | // a JSON number, that's an error. |
| 164 | func (obj JSONObject) GetFloat64() (value float64, err error) { |
| 165 | value, ok := obj.value.(float64) |
| 166 | if !ok { |
| 167 | err = failConversion("float64", obj) |
| 168 | } |
| 169 | return |
| 170 | } |
| 171 | |
| 172 | // GetMap retrieves the object's value as a map. If the value wasn't a JSON |
| 173 | // object, that's an error. |
| 174 | func (obj JSONObject) GetMap() (value map[string]JSONObject, err error) { |
| 175 | value, ok := obj.value.(map[string]JSONObject) |
| 176 | if !ok { |
| 177 | err = failConversion("map", obj) |
| 178 | } |
| 179 | return |
| 180 | } |
| 181 | |
| 182 | // GetArray retrieves the object's value as an array. If the value wasn't a |
| 183 | // JSON list, that's an error. |
| 184 | func (obj JSONObject) GetArray() (value []JSONObject, err error) { |
| 185 | value, ok := obj.value.([]JSONObject) |
| 186 | if !ok { |
| 187 | err = failConversion("array", obj) |
| 188 | } |
| 189 | return |
| 190 | } |
| 191 | |
| 192 | // GetBool retrieves the object's value as a bool. If the value wasn't a JSON |
| 193 | // bool, that's an error. |
| 194 | func (obj JSONObject) GetBool() (value bool, err error) { |
| 195 | value, ok := obj.value.(bool) |
| 196 | if !ok { |
| 197 | err = failConversion("bool", obj) |
| 198 | } |
| 199 | return |
| 200 | } |
| 201 | |
| 202 | // GetBytes retrieves the object's value as raw bytes. A JSONObject that was |
| 203 | // parsed from the original input (as opposed to one that's embedded in |
| 204 | // another JSONObject) can contain both the raw bytes and the parsed JSON |
| 205 | // value, but either can be the case without the other. |
| 206 | // If this object wasn't parsed directly from the original input, that's an |
| 207 | // error. |
| 208 | // If the object was parsed from an original input that just said "null", then |
| 209 | // IsNil will return true but the raw bytes are still available from GetBytes. |
| 210 | func (obj JSONObject) GetBytes() ([]byte, error) { |
| 211 | if obj.bytes == nil { |
| 212 | return nil, failConversion("bytes", obj) |
| 213 | } |
| 214 | return obj.bytes, nil |
| 215 | } |