blob: 5e11f00f382af0f3d5435f36a60ddf714a03ca59 [file] [log] [blame]
// Copyright 2020 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 build
import (
"context"
"reflect"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/structpb"
"go.chromium.org/luci/common/errors"
)
type inputPropertyReservations struct {
locs resLocations
msgFactories map[string]func() proto.Message
}
func (i *inputPropertyReservations) reserve(ns string, mkMsg func() proto.Message, skip int) {
i.locs.reserve(ns, "PropertyReader", skip+1)
if i.msgFactories == nil {
i.msgFactories = map[string]func() proto.Message{}
}
i.msgFactories[ns] = mkMsg
}
func (i *inputPropertyReservations) each(cb func(ns string, mkMsg func() proto.Message)) {
i.locs.each(func(ns string) {
cb(ns, i.msgFactories[ns])
})
}
func (i *inputPropertyReservations) clear() {
i.locs.clear(func() {
i.msgFactories = nil
})
}
var propReaderReservations = inputPropertyReservations{}
// MakePropertyReader allows your library/module to reserve a section of the
// input properties for itself.
//
// Attempting to reserve duplicate namespaces will panic. The namespace refers
// to the top-level property key. It is recommended that:
// * The `ns` begins with '$'.
// * The value after the '$' is the canonical Go package name for your
// library.
//
// Using the generated function will parse the relevant input property namespace
// as JSONPB, returning the parsed message (and an error, if any).
//
// var myPropertyReader func(context.Context) *MyPropertyMsg
// func init() {
// MakePropertyReader("$some/namespace", &myPropertyReader)
// }
//
// In Go2 this will be less weird:
// MakePropertyReader[T proto.Message](ns string) func(context.Context) T
func MakePropertyReader(ns string, fnptr interface{}) {
fn, msgT := getReaderFnValue(fnptr)
mkMsg := func() proto.Message {
return msgT.New().Interface()
}
propReaderReservations.reserve(ns, mkMsg, 1)
fn.Set(reflect.MakeFunc(fn.Type(), func(args []reflect.Value) []reflect.Value {
cstate := getState(args[0].Interface().(context.Context))
var msg proto.Message
if st := cstate.state; st != nil && st.reservedInputProperties[ns] != nil {
msg = proto.Clone(st.reservedInputProperties[ns])
} else {
msg = mkMsg().ProtoReflect().Type().Zero().Interface()
}
return []reflect.Value{reflect.ValueOf(msg)}
}))
}
// getReaderFnValue returns the reflect.Value of the underlying function in the
// pointer-to-function `fntptr`, as well as a function to construct `fnptr`'s
// concrete proto.Message return type.
func getReaderFnValue(fnptr interface{}) (reflect.Value, protoreflect.Message) {
return derefFnPtr(
errors.New("fnptr is not `func[T proto.Message](context.Context) T`"),
fnptr,
[]reflect.Type{ctxType},
[]reflect.Type{protoMessageType},
)
}
func parseReservedInputProperties(props *structpb.Struct, strict bool) (map[string]proto.Message, error) {
ret := map[string]proto.Message{}
merr := errors.MultiError{}
propReaderReservations.each(func(ns string, mkMsg func() proto.Message) {
propVal := props.GetFields()[ns]
if propVal == nil {
return
}
json, err := protojson.Marshal(propVal)
if err != nil {
panic(err) // this should be impossible
}
msg := mkMsg()
unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: !strict}
if err = unmarshaler.Unmarshal(json, msg); err != nil {
merr = append(merr, errors.Annotate(err, "deserializing input property %q", ns).Err())
return
}
ret[ns] = msg
})
var err error
if len(merr) > 0 {
err = merr
}
return ret, err
}
func parseTopLevelProperties(props *structpb.Struct, strict bool, dst proto.Message) error {
dstR := dst.ProtoReflect()
reservedLocs := propReaderReservations.locs.snap()
// first check if `reserved` overlaps with the fields in `dst`
fields := dstR.Descriptor().Fields()
for i := 0; i < fields.Len(); i++ {
field := fields.Get(i)
for _, conflict := range []string{field.TextName(), field.JSONName()} {
if loc := reservedLocs[conflict]; loc != "" {
return errors.Reason(
"use of top-level property message %T conflicts with MakePropertyReader(ns=%q) reserved at: %s",
dst, conflict, loc).Err()
}
}
}
// next, clone `props` and remove all fields which have been parsed
props = proto.Clone(props).(*structpb.Struct)
for ns := range reservedLocs {
delete(props.GetFields(), ns)
}
json, err := protojson.Marshal(props)
if err != nil {
panic(err) // this should be impossible
}
return protojson.UnmarshalOptions{DiscardUnknown: !strict}.Unmarshal(json, dst)
}