blob: a39a4c6a4f0d5c2dd4709001d643b9a5462945fa [file] [log] [blame]
// Copyright 2019 The LUCI 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.
package exe
import (
"bytes"
"encoding/json"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
structpb "github.com/golang/protobuf/ptypes/struct"
"go.chromium.org/luci/common/errors"
)
// ParseProperties interprets a protobuf 'struct' as structured Go data.
//
// `outputs` is a mapping of a property name to an output structure. An output
// structure may be one of two things:
//
// * a non-nil proto.Message. The data in this field will be interpreted as
// JSONPB and Unmarshaled into the proto.Message.
// * a valid "encoding/json" unmarshal target. The data in this field will be
// unmarshaled into with the stdlib "encoding/json" package.
//
// This function will scan the props (usually `build.Input.Properties`) and
// unmarshal them as appropriate into the outputs.
//
// Example:
//
// myProto := &myprotos.Message{}
// myStruct := &MyStruct{}
// err := ParseProperties(build.Input.Properties, map[string]interface{}{
// "proto": myProto, "$namespaced/struct": myStruct})
// // handle err :)
// fmt.Println("Got:", myProto.Field)
// fmt.Println("Got:", myStruct.Field)
func ParseProperties(props *structpb.Struct, outputs map[string]interface{}) error {
ret := errors.NewLazyMultiError(len(outputs))
idx := -1
for field, output := range outputs {
idx++
val := props.Fields[field]
if val == nil {
continue
}
var jsonBuf bytes.Buffer
if err := (&jsonpb.Marshaler{}).Marshal(&jsonBuf, val); err != nil {
ret.Assign(idx, errors.Annotate(err, "marshaling %q", field).Err())
continue
}
var err error
switch x := output.(type) {
case proto.Message:
err = jsonpb.Unmarshal(&jsonBuf, x)
default:
err = json.NewDecoder(&jsonBuf).Decode(x)
}
ret.Assign(idx, errors.Annotate(err, "unmarshalling %q", field).Err())
}
return ret.Get()
}
type nullType struct{}
// Null is a sentinel value to assign JSON `null` to a property with
// WriteProperties.
var Null = nullType{}
// WriteProperties updates a protobuf 'struct' with structured Go data.
//
// `inputs` is a mapping of a property name to an input structure. An input
// structure may be one of two things:
//
// * a non-nil proto.Message. The data in this field will be interpreted as
// JSONPB and Unmarshaled into the proto.Message.
// * a valid "encoding/json" marshal source. The data in this field will be
// interpreted as json and marshaled with the stdlib "encoding/json" package.
// * The `Null` value in this package. The top-level property will be set to
// JSON `null`.
// * nil. The top-level property will be removed.
//
// This function will scan the inputs and marshal them as appropriate into
// `props` (usually `build.Output.Properties`).
//
// Example:
//
// myProto := &myprotos.Message{Field: "something"}
// myStruct := &MyStruct{Field: 100}
// err := WriteProperties(build.Output.Properties, map[string]interface{}{
// "proto": myProto, "$namespaced/struct": myStruct})
// // handle err :)
func WriteProperties(props *structpb.Struct, inputs map[string]interface{}) error {
if props.Fields == nil {
props.Fields = make(map[string]*structpb.Value, len(inputs))
}
ret := errors.NewLazyMultiError(len(inputs))
idx := -1
for field, input := range inputs {
idx++
if input == nil {
delete(props.Fields, field)
continue
}
var buf bytes.Buffer
var err error
fieldVal := props.Fields[field]
if fieldVal == nil {
fieldVal = &structpb.Value{}
props.Fields[field] = fieldVal
}
switch x := input.(type) {
case nullType:
fieldVal.Kind = &structpb.Value_NullValue{}
continue
case proto.Message:
err = (&jsonpb.Marshaler{OrigName: true}).Marshal(&buf, x)
default:
var data []byte
data, err = json.Marshal(x)
buf.Write(data)
}
if err != nil {
ret.Assign(idx, errors.Annotate(err, "marshaling %q", field).Err())
continue
}
ret.Assign(idx, errors.Annotate(
jsonpb.Unmarshal(&buf, fieldVal), "unmarshalling %q", field,
).Err())
}
return ret.Get()
}