blob: dd244e0054140bf5fb272fe887d78694f41d947a [file] [log] [blame]
// Copyright 2022 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 protowalk
import (
"fmt"
"reflect"
"google.golang.org/protobuf/reflect/protoreflect"
)
// FieldSelector is called once per field per message type per process and
// the result is cached by the type name of this FieldProcessor (i.e.
// reflect.TypeOf to observe the package and local type name of the processor)
// and the full proto message name.
//
// Returns an enum of how this processor wants to handle the provided field.
//
// This function is registered with a corresponding FieldProcessor in
// RegisterFieldProcessor.
type FieldSelector func(field protoreflect.FieldDescriptor) ProcessAttr
// RegisterFieldProcessor registers a new FieldProcessor to allow it to be used
// with protowalk.Fields.
//
// This should be called once per FieldProcessor, per process like:
//
// func init() {
// protowalk.RegisterFieldProcessor(&MyFP{}, MyFPFieldSelector)
// }
//
// Calling RegisterFieldProcessor twice for the same FieldProcessor will panic.
func RegisterFieldProcessor(fp FieldProcessor, selector FieldSelector) {
fieldProcessorSelectorsMu.Lock()
defer fieldProcessorSelectorsMu.Unlock()
t := reflect.TypeOf(fp)
if fieldProcessorSelectors == nil {
fieldProcessorSelectors = make(map[reflect.Type]FieldSelector, 10)
}
if fieldProcessorSelectors[t] != nil {
panic(fmt.Sprintf("FieldProcessor %T already registered", fp))
}
fieldProcessorSelectors[t] = selector
}
// FieldProcessor allows processing a set of proto message fields in conjunction
// with the package-level Fields() function.
//
// Typically FieldProcessor implementations will apply to fields with particular
// annotations, but a FieldProcessor can technically react to any field(s) that
// it wants to.
type FieldProcessor interface {
// Process will only be called on fields where the registered FieldSelector
// function already returned a non-zero ProcessAttr value.
//
// Process will never be invoked for a field on a nil message. That is,
// technically, someMessage.someField is 'unset', even if someMessage is nil.
// Even if the FieldSelector returned ProccessUnset, it would still not be
// called on someField.
//
// If `applied` == true, `data` will be included in the Results from
// protowalk.Fields.
//
// It is allowed for Process to mutate the value of `field` in `msg`, but
// mutating other fields is undefined behavior.
//
// When processing a given message, an instance of FieldProcessor will have
// its Process method called sequentially per affected field, interspersed
// with other FieldProcessors in the same Fields call. For example, if you
// process a message with FieldProcessors A and B, where A processes evenly-
// numbered fields, and B processes oddly-numbered fields, the calls would
// look like:
// * B.Process(1)
// * A.Process(2)
// * B.Process(3)
//
// If two processors apply to the same field in a message, they'll be called
// in the order specified to Fields (i.e. Fields(..., A{}, B{}) would call AS
// then B, and Fields(..., B{}, A{}) would call B then A).
Process(field protoreflect.FieldDescriptor, msg protoreflect.Message) (data ResultData, applied bool)
}