protowalk

package
v0.0.0-...-7bf56e6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 3, 2024 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package protowalk contains routines for efficiently walking proto messages, focusing on the ability to react to field annotations.

This package defines an interface (FieldProcessor) which you can implement to react to any fields you like.

This package also comes with stock implementations for the following common field annotations:

  • google.api.field_behavior = REQUIRED
  • google.api.field_behavior = OUTPUT_ONLY
  • deprecated = true

How it works

1. Register a FieldProcessor using RegisterFieldProcessor.

A FieldProcessor checks and/or manipulates fields in a proto message. When registering an implementation, you also provide a `selector` function which detects fields (e.g. by their type, field options, etc.) that the FieldProcessor wants to operate on.

2. Use the Fields() with one or more FieldProcessor instantances to process a proto message.

Fields generates, once per process, a global cache entry keyed on (msgType, processorType). This entry is a list of fields in `msgType` which the FieldProcessor's registered `selector` function says it should operate on, and also indicates for a given field if it should be recursed for this processor (i.e. if field `M.a` is a message type `A` which has fields that match this processor).

Once the cache entries are generated, Fields then generates a 'plan' which is the combination of _all_ the provided processors' cache entries, such that Fields() can then walk through all affected fields, once, in order, generating results from the provided message in an ordered and deterministic fashion.

The upshot is that Fields() should be O(CheckableFields), no matter how big the proto messages are, rather than a naive O(Fields) implementation.

A Result contains the ProtoPath and a message generated by the FieldProcessor. The ProtoPath describes the path through the source message to the affected field, and can either be printed for display, or can be used to retrieve the actual value.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RegisterFieldProcessor

func RegisterFieldProcessor(fp FieldProcessor, selector FieldSelector)

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.

Types

type DeprecatedProcessor

type DeprecatedProcessor struct {
	// If true, clear the data out of all deprecated fields.
	ClearField bool
}

DeprecatedProcessor will find, and produce a Result for every deprecated field which is set. Deprecated fields are marked like:

type name = <tag> [deprecated = true];

You may also optionally clear the data in deprecated fields.

func (*DeprecatedProcessor) Process

func (d *DeprecatedProcessor) Process(field protoreflect.FieldDescriptor, msg protoreflect.Message) (data ResultData, applied bool)

Process implements FieldProcessor.

This will optionally clear (zero) the field in `msg` to avoid accidental usage of deprecated fields

type FieldProcessor

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)
}

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 FieldSelector

type FieldSelector func(field protoreflect.FieldDescriptor) ProcessAttr

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 OutputOnlyProcessor

type OutputOnlyProcessor struct{}

OutputOnlyProcessor implements the recommended behavior of aip.dev/203#output-only, namely that fields marked like:

type name = <tag> [(google.api.field_behavior) = OUTPUT_ONLY];

Should be cleared by the server (without raising an error) if a user request includes a value for them.

func (OutputOnlyProcessor) Process

Process implements FieldProcessor.

This will clear (zero) the field in `msg`.

type ProcessAttr

type ProcessAttr byte

ProcessAttr is a cache data value which indicates that this field needs to be processed, whether for set fields, unset fields, or both.

const (
	// ProcessNever indicates that this processor doesn't want to process this
	// field.
	ProcessNever ProcessAttr = 0b00

	// ProcessIfSet indicates that this processor only applies to this field when
	// the field has a value (i.e. protoreflect.Message.Has(field)).
	ProcessIfSet ProcessAttr = 0b01

	// ProcessIfUnset indicates that this processor only applies to this field when
	// the field does not have a value (i.e. !protoreflect.Message.Has(field)).
	ProcessIfUnset ProcessAttr = 0b10

	// ProcessAlways indicates that this processor always applies to this field
	// regardless of value status.
	ProcessAlways = ProcessIfSet | ProcessIfUnset
)

func (ProcessAttr) Valid

func (a ProcessAttr) Valid() bool

Valid returns true if `a` is a known ProcessAttr value.

type RequiredProcessor

type RequiredProcessor struct{}

RequiredProcessor implements the recommended behavior of aip.dev/203#required, namely that it produces a Report for unset fields marked like:

type name = <tag> [(google.api.field_behavior) = REQUIRED];

NOTE: In LUCI code, it's recommended to use "protoc-gen-validate" (i.e. PGV) annotations to indicate required fields. The PGV generated code is easier to use and (likely) more efficient than the implementation in this package.

func (RequiredProcessor) Process

Process implements FieldProcessor.

type Result

type Result struct {
	// Path is the path to the field where the FieldProcessor acted.
	Path reflectutil.Path

	Data ResultData
}

Result is a singluar outcome from a FieldProcessor.Process which acted on a message field.

func (Result) Err

func (r Result) Err() error

func (Result) String

func (r Result) String() string

type ResultData

type ResultData struct {
	// Message is a human-readable message from FieldProcessor.Process describing
	// the nature of the violation or correction (depends on the FieldProcessor
	// implementation).
	Message string

	// IsErr is a flag for if the processed result should be considered as an error.
	IsErr bool
}

ResultData is the data produced from the FieldProcessor.

func (ResultData) String

func (d ResultData) String() string

type Results

type Results [][]Result

Results holds a slice of Result structs for each FieldProcessor passed in.

func Fields

func Fields(msg proto.Message, processors ...FieldProcessor) Results

Fields recursively processes the fields of `msg` with the given sequence of FieldProcessors.

func (Results) Err

func (r Results) Err() error

Err returns the error for all processors.

func (Results) Strings

func (r Results) Strings() []string

Strings returns a flat list of strings for all processors.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL