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  }