blob: af2ce76a33d91db30eb9eecf1efc85793247be86 [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This package enables unmarshaling YAML strings into protobuf messages. We do
// this by using the jsonpb package. We first marshal the YAML string into go
// types unsing the go-yaml package. Then, we convert those to json-compatible
// types. We then marshal this data structure to JSON and finally we use jsonpb
// to unmarshal that string to the protobuf message.
package yamlpb
import (
"encoding/json"
"fmt"
yaml "github.com/go-yaml/yaml"
jsonpb "github.com/golang/protobuf/jsonpb"
proto "github.com/golang/protobuf/proto"
"strconv"
)
// toJsonCompatibleValue recursively converts the YAML-compatible value to a
// JSON-comptaible value if possible.
func toJsonCompatibleValue(i interface{}) (o interface{}, err error) {
switch t := i.(type) {
case map[interface{}]interface{}:
return toStrMap(t)
case []byte:
return t, nil
case []interface{}:
var arr []interface{}
for _, v := range t {
var j interface{}
j, err = toJsonCompatibleValue(v)
if err != nil {
return nil, err
}
arr = append(arr, j)
}
return arr, nil
}
return i, nil
}
// toStrMap creates a string-indexed map based on the provided map if possible.
// YAML supports many different kinds of indices in maps. JSON only supports
// string-indexed maps and protobuf only supports string and integer-indexed
// maps. So, we convert integer-indexed maps to string maps according to
// https://developers.google.com/protocol-buffers/docs/proto3#json
// and recursively convert values to JSON-compatible types.
func toStrMap(i map[interface{}]interface{}) (o map[string]interface{}, err error) {
o = make(map[string]interface{})
for k, v := range i {
var s string
switch t := k.(type) {
case string:
s = t
case int:
s = strconv.FormatInt(int64(t), 10)
default:
err = fmt.Errorf("'%v' is not a string or integer. Only string and integer keys are supported.", k)
return nil, err
}
o[s], err = toJsonCompatibleValue(v)
if err != nil {
return nil, err
}
}
return o, nil
}
// UnmarshalString will populate the fields of a protocol buffer based on a YAML
// string.
func UnmarshalString(s string, pb proto.Message) error {
// First, we unmarshal the yaml string into go types.
var m interface{}
if err := yaml.Unmarshal([]byte(s), &m); err != nil {
return fmt.Errorf("Cannot unmarshal yaml string: %v", err)
}
// Then, we ensure that only JSON-compatible values are used since YAML
// is a superset of JSON.
v, err := toJsonCompatibleValue(m)
if err != nil {
return err
}
// We marshal to JSON.
var j []byte
j, err = json.Marshal(v)
if err != nil {
return err
}
// And finally, we unmarshal to proto.
if err := jsonpb.UnmarshalString(string(j), pb); err != nil {
return err
}
return nil
}
// MarshalString marshals a protobuf message to a YAML string.
func MarshalString(pb proto.Message) (string, error) {
// First, we marshal proto to JSON to recover the original field names.
// We need to do this because if we marshal the proto.Message directly to
// YAML, it will use the mangled-for-go names.
ma := jsonpb.Marshaler{
EnumsAsInts: false,
EmitDefaults: false,
OrigName: true,
}
js, err := ma.MarshalToString(pb)
if err != nil {
return "", err
}
// Then we unmarshal from JSON.
var j interface{}
if err := json.Unmarshal([]byte(js), &j); err != nil {
return "", err
}
// Finally, we marshal to YAML.
var y []byte
y, err = yaml.Marshal(j)
if err != nil {
return "", err
}
return string(y), nil
}