sslobodr | d046be8 | 2019-01-16 10:02:22 -0500 | [diff] [blame^] | 1 | package yaml |
| 2 | |
| 3 | import ( |
| 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. |
| 15 | func 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. |
| 30 | func 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. |
| 46 | func 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. |
| 73 | func YAMLToJSON(y []byte) ([]byte, error) { |
| 74 | return yamlToJSON(y, nil) |
| 75 | } |
| 76 | |
| 77 | func 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 | |
| 98 | func 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 | } |