go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/exe/props.go (about)

     1  // Copyright 2019 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package exe
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  
    21  	"github.com/golang/protobuf/jsonpb"
    22  	"github.com/golang/protobuf/proto"
    23  	"google.golang.org/protobuf/types/known/structpb"
    24  
    25  	"go.chromium.org/luci/common/errors"
    26  )
    27  
    28  // ParseProperties interprets a protobuf 'struct' as structured Go data.
    29  //
    30  // `outputs` is a mapping of a property name to an output structure. An output
    31  // structure may be one of two things:
    32  //
    33  //   - a non-nil proto.Message. The data in this field will be interpreted as
    34  //     JSONPB and Unmarshaled into the proto.Message.
    35  //   - a valid "encoding/json" unmarshal target. The data in this field will be
    36  //     unmarshaled into with the stdlib "encoding/json" package.
    37  //
    38  // This function will scan the props (usually `build.Input.Properties`) and
    39  // unmarshal them as appropriate into the outputs.
    40  //
    41  // Example:
    42  //
    43  //	myProto := &myprotos.Message{}
    44  //	myStruct := &MyStruct{}
    45  //	err := ParseProperties(build.Input.Properties, map[string]any{
    46  //	  "proto": myProto, "$namespaced/struct": myStruct})
    47  //	// handle err :)
    48  //	fmt.Println("Got:", myProto.Field)
    49  //	fmt.Println("Got:", myStruct.Field)
    50  func ParseProperties(props *structpb.Struct, outputs map[string]any) error {
    51  	ret := errors.NewLazyMultiError(len(outputs))
    52  
    53  	idx := -1
    54  	for field, output := range outputs {
    55  		idx++
    56  
    57  		val := props.Fields[field]
    58  		if val == nil {
    59  			continue
    60  		}
    61  
    62  		var jsonBuf bytes.Buffer
    63  		if err := (&jsonpb.Marshaler{}).Marshal(&jsonBuf, val); err != nil {
    64  			ret.Assign(idx, errors.Annotate(err, "marshaling %q", field).Err())
    65  			continue
    66  		}
    67  
    68  		var err error
    69  		switch x := output.(type) {
    70  		case proto.Message:
    71  			err = jsonpb.Unmarshal(&jsonBuf, x)
    72  		default:
    73  			err = json.NewDecoder(&jsonBuf).Decode(x)
    74  		}
    75  
    76  		ret.Assign(idx, errors.Annotate(err, "unmarshalling %q", field).Err())
    77  	}
    78  
    79  	return ret.Get()
    80  }
    81  
    82  type nullType struct{}
    83  
    84  // Null is a sentinel value to assign JSON `null` to a property with
    85  // WriteProperties.
    86  var Null = nullType{}
    87  
    88  // WriteProperties updates a protobuf 'struct' with structured Go data.
    89  //
    90  // `inputs` is a mapping of a property name to an input structure. An input
    91  // structure may be one of two things:
    92  //
    93  //   - a non-nil proto.Message. The data in this field will be interpreted as
    94  //     JSONPB and Unmarshaled into the proto.Message.
    95  //   - a valid "encoding/json" marshal source. The data in this field will be
    96  //     interpreted as json and marshaled with the stdlib "encoding/json" package.
    97  //   - The `Null` value in this package. The top-level property will be set to
    98  //     JSON `null`.
    99  //   - nil. The top-level property will be removed.
   100  //
   101  // This function will scan the inputs and marshal them as appropriate into
   102  // `props` (usually `build.Output.Properties`).
   103  //
   104  // Example:
   105  //
   106  //	myProto := &myprotos.Message{Field: "something"}
   107  //	myStruct := &MyStruct{Field: 100}
   108  //	err := WriteProperties(build.Output.Properties, map[string]any{
   109  //	  "proto": myProto, "$namespaced/struct": myStruct})
   110  //	// handle err :)
   111  func WriteProperties(props *structpb.Struct, inputs map[string]any) error {
   112  	if props.Fields == nil {
   113  		props.Fields = make(map[string]*structpb.Value, len(inputs))
   114  	}
   115  
   116  	ret := errors.NewLazyMultiError(len(inputs))
   117  
   118  	idx := -1
   119  	for field, input := range inputs {
   120  		idx++
   121  
   122  		if input == nil {
   123  			delete(props.Fields, field)
   124  			continue
   125  		}
   126  
   127  		var buf bytes.Buffer
   128  		var err error
   129  
   130  		fieldVal := props.Fields[field]
   131  		if fieldVal == nil {
   132  			fieldVal = &structpb.Value{}
   133  			props.Fields[field] = fieldVal
   134  		}
   135  
   136  		switch x := input.(type) {
   137  		case nullType:
   138  			fieldVal.Kind = &structpb.Value_NullValue{}
   139  			continue
   140  
   141  		case proto.Message:
   142  			err = (&jsonpb.Marshaler{OrigName: true}).Marshal(&buf, x)
   143  
   144  		default:
   145  			var data []byte
   146  			data, err = json.Marshal(x)
   147  			buf.Write(data)
   148  		}
   149  
   150  		if err != nil {
   151  			ret.Assign(idx, errors.Annotate(err, "marshaling %q", field).Err())
   152  			continue
   153  		}
   154  
   155  		ret.Assign(idx, errors.Annotate(
   156  			jsonpb.Unmarshal(&buf, fieldVal), "unmarshalling %q", field,
   157  		).Err())
   158  	}
   159  
   160  	return ret.Get()
   161  }