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 }