blob: 4fb4054a8b74711fcd7142b05f95923c9f715449 [file] [log] [blame]
Zack Williamse940c7a2019-08-21 14:25:39 -07001package yaml
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "reflect"
8 "strconv"
9
10 "gopkg.in/yaml.v2"
11)
12
13// Marshals the object into JSON then converts JSON to YAML and returns the
14// YAML.
15func Marshal(o interface{}) ([]byte, error) {
16 j, err := json.Marshal(o)
17 if err != nil {
18 return nil, fmt.Errorf("error marshaling into JSON: %v", err)
19 }
20
21 y, err := JSONToYAML(j)
22 if err != nil {
23 return nil, fmt.Errorf("error converting JSON to YAML: %v", err)
24 }
25
26 return y, nil
27}
28
29// Converts YAML to JSON then uses JSON to unmarshal into an object.
30func Unmarshal(y []byte, o interface{}) error {
31 vo := reflect.ValueOf(o)
32 j, err := yamlToJSON(y, &vo)
33 if err != nil {
34 return fmt.Errorf("error converting YAML to JSON: %v", err)
35 }
36
37 err = json.Unmarshal(j, o)
38 if err != nil {
39 return fmt.Errorf("error unmarshaling JSON: %v", err)
40 }
41
42 return nil
43}
44
45// Convert JSON to YAML.
46func JSONToYAML(j []byte) ([]byte, error) {
47 // Convert the JSON to an object.
48 var jsonObj interface{}
49 // We are using yaml.Unmarshal here (instead of json.Unmarshal) because the
50 // Go JSON library doesn't try to pick the right number type (int, float,
51 // etc.) when unmarshalling to interface{}, it just picks float64
52 // universally. go-yaml does go through the effort of picking the right
53 // number type, so we can preserve number type throughout this process.
54 err := yaml.Unmarshal(j, &jsonObj)
55 if err != nil {
56 return nil, err
57 }
58
59 // Marshal this object into YAML.
60 return yaml.Marshal(jsonObj)
61}
62
63// Convert YAML to JSON. Since JSON is a subset of YAML, passing JSON through
64// this method should be a no-op.
65//
66// Things YAML can do that are not supported by JSON:
67// * In YAML you can have binary and null keys in your maps. These are invalid
68// in JSON. (int and float keys are converted to strings.)
69// * Binary data in YAML with the !!binary tag is not supported. If you want to
70// use binary data with this library, encode the data as base64 as usual but do
71// not use the !!binary tag in your YAML. This will ensure the original base64
72// encoded data makes it all the way through to the JSON.
73func YAMLToJSON(y []byte) ([]byte, error) {
74 return yamlToJSON(y, nil)
75}
76
77func yamlToJSON(y []byte, jsonTarget *reflect.Value) ([]byte, error) {
78 // Convert the YAML to an object.
79 var yamlObj interface{}
80 err := yaml.Unmarshal(y, &yamlObj)
81 if err != nil {
82 return nil, err
83 }
84
85 // YAML objects are not completely compatible with JSON objects (e.g. you
86 // can have non-string keys in YAML). So, convert the YAML-compatible object
87 // to a JSON-compatible object, failing with an error if irrecoverable
88 // incompatibilties happen along the way.
89 jsonObj, err := convertToJSONableObject(yamlObj, jsonTarget)
90 if err != nil {
91 return nil, err
92 }
93
94 // Convert this object to JSON and return the data.
95 return json.Marshal(jsonObj)
96}
97
98func convertToJSONableObject(yamlObj interface{}, jsonTarget *reflect.Value) (interface{}, error) {
99 var err error
100
101 // Resolve jsonTarget to a concrete value (i.e. not a pointer or an
102 // interface). We pass decodingNull as false because we're not actually
103 // decoding into the value, we're just checking if the ultimate target is a
104 // string.
105 if jsonTarget != nil {
106 ju, tu, pv := indirect(*jsonTarget, false)
107 // We have a JSON or Text Umarshaler at this level, so we can't be trying
108 // to decode into a string.
109 if ju != nil || tu != nil {
110 jsonTarget = nil
111 } else {
112 jsonTarget = &pv
113 }
114 }
115
116 // If yamlObj is a number or a boolean, check if jsonTarget is a string -
117 // if so, coerce. Else return normal.
118 // If yamlObj is a map or array, find the field that each key is
119 // unmarshaling to, and when you recurse pass the reflect.Value for that
120 // field back into this function.
121 switch typedYAMLObj := yamlObj.(type) {
122 case map[interface{}]interface{}:
123 // JSON does not support arbitrary keys in a map, so we must convert
124 // these keys to strings.
125 //
126 // From my reading of go-yaml v2 (specifically the resolve function),
127 // keys can only have the types string, int, int64, float64, binary
128 // (unsupported), or null (unsupported).
129 strMap := make(map[string]interface{})
130 for k, v := range typedYAMLObj {
131 // Resolve the key to a string first.
132 var keyString string
133 switch typedKey := k.(type) {
134 case string:
135 keyString = typedKey
136 case int:
137 keyString = strconv.Itoa(typedKey)
138 case int64:
139 // go-yaml will only return an int64 as a key if the system
140 // architecture is 32-bit and the key's value is between 32-bit
141 // and 64-bit. Otherwise the key type will simply be int.
142 keyString = strconv.FormatInt(typedKey, 10)
143 case float64:
144 // Stolen from go-yaml to use the same conversion to string as
145 // the go-yaml library uses to convert float to string when
146 // Marshaling.
147 s := strconv.FormatFloat(typedKey, 'g', -1, 32)
148 switch s {
149 case "+Inf":
150 s = ".inf"
151 case "-Inf":
152 s = "-.inf"
153 case "NaN":
154 s = ".nan"
155 }
156 keyString = s
157 case bool:
158 if typedKey {
159 keyString = "true"
160 } else {
161 keyString = "false"
162 }
163 default:
164 return nil, fmt.Errorf("Unsupported map key of type: %s, key: %+#v, value: %+#v",
165 reflect.TypeOf(k), k, v)
166 }
167
168 // jsonTarget should be a struct or a map. If it's a struct, find
169 // the field it's going to map to and pass its reflect.Value. If
170 // it's a map, find the element type of the map and pass the
171 // reflect.Value created from that type. If it's neither, just pass
172 // nil - JSON conversion will error for us if it's a real issue.
173 if jsonTarget != nil {
174 t := *jsonTarget
175 if t.Kind() == reflect.Struct {
176 keyBytes := []byte(keyString)
177 // Find the field that the JSON library would use.
178 var f *field
179 fields := cachedTypeFields(t.Type())
180 for i := range fields {
181 ff := &fields[i]
182 if bytes.Equal(ff.nameBytes, keyBytes) {
183 f = ff
184 break
185 }
186 // Do case-insensitive comparison.
187 if f == nil && ff.equalFold(ff.nameBytes, keyBytes) {
188 f = ff
189 }
190 }
191 if f != nil {
192 // Find the reflect.Value of the most preferential
193 // struct field.
194 jtf := t.Field(f.index[0])
195 strMap[keyString], err = convertToJSONableObject(v, &jtf)
196 if err != nil {
197 return nil, err
198 }
199 continue
200 }
201 } else if t.Kind() == reflect.Map {
202 // Create a zero value of the map's element type to use as
203 // the JSON target.
204 jtv := reflect.Zero(t.Type().Elem())
205 strMap[keyString], err = convertToJSONableObject(v, &jtv)
206 if err != nil {
207 return nil, err
208 }
209 continue
210 }
211 }
212 strMap[keyString], err = convertToJSONableObject(v, nil)
213 if err != nil {
214 return nil, err
215 }
216 }
217 return strMap, nil
218 case []interface{}:
219 // We need to recurse into arrays in case there are any
220 // map[interface{}]interface{}'s inside and to convert any
221 // numbers to strings.
222
223 // If jsonTarget is a slice (which it really should be), find the
224 // thing it's going to map to. If it's not a slice, just pass nil
225 // - JSON conversion will error for us if it's a real issue.
226 var jsonSliceElemValue *reflect.Value
227 if jsonTarget != nil {
228 t := *jsonTarget
229 if t.Kind() == reflect.Slice {
230 // By default slices point to nil, but we need a reflect.Value
231 // pointing to a value of the slice type, so we create one here.
232 ev := reflect.Indirect(reflect.New(t.Type().Elem()))
233 jsonSliceElemValue = &ev
234 }
235 }
236
237 // Make and use a new array.
238 arr := make([]interface{}, len(typedYAMLObj))
239 for i, v := range typedYAMLObj {
240 arr[i], err = convertToJSONableObject(v, jsonSliceElemValue)
241 if err != nil {
242 return nil, err
243 }
244 }
245 return arr, nil
246 default:
247 // If the target type is a string and the YAML type is a number,
248 // convert the YAML type to a string.
249 if jsonTarget != nil && (*jsonTarget).Kind() == reflect.String {
250 // Based on my reading of go-yaml, it may return int, int64,
251 // float64, or uint64.
252 var s string
253 switch typedVal := typedYAMLObj.(type) {
254 case int:
255 s = strconv.FormatInt(int64(typedVal), 10)
256 case int64:
257 s = strconv.FormatInt(typedVal, 10)
258 case float64:
259 s = strconv.FormatFloat(typedVal, 'g', -1, 32)
260 case uint64:
261 s = strconv.FormatUint(typedVal, 10)
262 case bool:
263 if typedVal {
264 s = "true"
265 } else {
266 s = "false"
267 }
268 }
269 if len(s) > 0 {
270 yamlObj = interface{}(s)
271 }
272 }
273 return yamlObj, nil
274 }
275
276 return nil, nil
277}