go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/build/input_properties.go (about) 1 // Copyright 2020 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 build 16 17 import ( 18 "context" 19 "reflect" 20 21 "google.golang.org/protobuf/encoding/protojson" 22 "google.golang.org/protobuf/proto" 23 "google.golang.org/protobuf/reflect/protoreflect" 24 "google.golang.org/protobuf/types/known/structpb" 25 26 "go.chromium.org/luci/common/errors" 27 ) 28 29 type inputPropertyReservations struct { 30 locs resLocations 31 msgFactories map[string]func() proto.Message 32 } 33 34 func (i *inputPropertyReservations) reserve(ns string, mkMsg func() proto.Message, skip int) { 35 i.locs.reserve(ns, "PropertyReader", skip+1) 36 if i.msgFactories == nil { 37 i.msgFactories = map[string]func() proto.Message{} 38 } 39 i.msgFactories[ns] = mkMsg 40 } 41 42 func (i *inputPropertyReservations) each(cb func(ns string, mkMsg func() proto.Message)) { 43 i.locs.each(func(ns string) { 44 cb(ns, i.msgFactories[ns]) 45 }) 46 } 47 48 func (i *inputPropertyReservations) clear() { 49 i.locs.clear(func() { 50 i.msgFactories = nil 51 }) 52 } 53 54 var propReaderReservations = inputPropertyReservations{} 55 56 // MakePropertyReader allows your library/module to reserve a section of the 57 // input properties for itself. 58 // 59 // Attempting to reserve duplicate namespaces will panic. The namespace refers 60 // to the top-level property key. It is recommended that: 61 // - The `ns` begins with '$'. 62 // - The value after the '$' is the canonical Go package name for your 63 // library. 64 // 65 // Using the generated function will parse the relevant input property namespace 66 // as JSONPB, returning the parsed message (and an error, if any). 67 // 68 // var myPropertyReader func(context.Context) *MyPropertyMsg 69 // func init() { 70 // MakePropertyReader("$some/namespace", &myPropertyReader) 71 // } 72 // 73 // In Go2 this will be less weird: 74 // 75 // MakePropertyReader[T proto.Message](ns string) func(context.Context) T 76 func MakePropertyReader(ns string, fnptr any) { 77 fn, msgT := getReaderFnValue(fnptr) 78 mkMsg := func() proto.Message { 79 return msgT.New().Interface() 80 } 81 propReaderReservations.reserve(ns, mkMsg, 1) 82 83 fn.Set(reflect.MakeFunc(fn.Type(), func(args []reflect.Value) []reflect.Value { 84 cstate := getState(args[0].Interface().(context.Context)) 85 var msg proto.Message 86 87 if st := cstate.state; st != nil && st.reservedInputProperties[ns] != nil { 88 msg = proto.Clone(st.reservedInputProperties[ns]) 89 } else { 90 msg = mkMsg().ProtoReflect().Type().Zero().Interface() 91 } 92 93 return []reflect.Value{reflect.ValueOf(msg)} 94 })) 95 } 96 97 // getReaderFnValue returns the reflect.Value of the underlying function in the 98 // pointer-to-function `fntptr`, as well as a function to construct `fnptr`'s 99 // concrete proto.Message return type. 100 func getReaderFnValue(fnptr any) (reflect.Value, protoreflect.Message) { 101 return derefFnPtr( 102 errors.New("fnptr is not `func[T proto.Message](context.Context) T`"), 103 fnptr, 104 []reflect.Type{ctxType}, 105 []reflect.Type{protoMessageType}, 106 ) 107 } 108 109 func parseReservedInputProperties(props *structpb.Struct, strict bool) (map[string]proto.Message, error) { 110 ret := map[string]proto.Message{} 111 merr := errors.MultiError{} 112 113 propReaderReservations.each(func(ns string, mkMsg func() proto.Message) { 114 propVal := props.GetFields()[ns] 115 if propVal == nil { 116 return 117 } 118 119 json, err := protojson.Marshal(propVal) 120 if err != nil { 121 panic(err) // this should be impossible 122 } 123 124 msg := mkMsg() 125 unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: !strict} 126 if err = unmarshaler.Unmarshal(json, msg); err != nil { 127 merr = append(merr, errors.Annotate(err, "deserializing input property %q", ns).Err()) 128 return 129 } 130 131 ret[ns] = msg 132 }) 133 134 var err error 135 if len(merr) > 0 { 136 err = merr 137 } 138 139 return ret, err 140 } 141 142 func parseTopLevelProperties(props *structpb.Struct, strict bool, dst proto.Message) error { 143 dstR := dst.ProtoReflect() 144 reservedLocs := propReaderReservations.locs.snap() 145 146 // first check if `reserved` overlaps with the fields in `dst` 147 fields := dstR.Descriptor().Fields() 148 for i := 0; i < fields.Len(); i++ { 149 field := fields.Get(i) 150 for _, conflict := range []string{field.TextName(), field.JSONName()} { 151 if loc := reservedLocs[conflict]; loc != "" { 152 return errors.Reason( 153 "use of top-level property message %T conflicts with MakePropertyReader(ns=%q) reserved at: %s", 154 dst, conflict, loc).Err() 155 } 156 } 157 } 158 159 // next, clone `props` and remove all fields which have been parsed 160 props = proto.Clone(props).(*structpb.Struct) 161 for ns := range reservedLocs { 162 delete(props.GetFields(), ns) 163 } 164 165 json, err := protojson.Marshal(props) 166 if err != nil { 167 panic(err) // this should be impossible 168 } 169 170 return protojson.UnmarshalOptions{DiscardUnknown: !strict}.Unmarshal(json, dst) 171 }