blob: cdd3dc13999329c6d6f1c461ff5dfb25e05c3960 [file] [log] [blame]
David K. Bainbridge528b3182017-01-23 08:51:59 -08001// Copyright 2012-2016 Canonical Ltd.
2// Licensed under the LGPLv3, see LICENCE file for details.
3
4package gomaasapi
5
6import (
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().
27type 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.)
51const 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.
57func 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.
84func 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
108func 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.
117func 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.
124func (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.
132var _ 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.
140func (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.
154func (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.
164func (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.
174func (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.
184func (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.
194func (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.
210func (obj JSONObject) GetBytes() ([]byte, error) {
211 if obj.bytes == nil {
212 return nil, failConversion("bytes", obj)
213 }
214 return obj.bytes, nil
215}