| // 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" |
| "google.golang.org/protobuf/types/known/structpb" |
| |
| "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() |
| } |