Documentation
¶
Overview ¶
Package pbpath provides functionality for representing a sequence of protobuf reflection operations on a message, including parsing human-readable path strings and traversing messages along a path to collect values.
pbpath extends the standard protopath.Step with additional step kinds for list range slicing ([start:end], [start:], [:end]) and list wildcards ([*], [:]) that fan out during value traversal.
Index ¶
- func FormatValue(v protoreflect.Value, fd protoreflect.FieldDescriptor) string
- func PathValuesMulti(md protoreflect.MessageDescriptor, m proto.Message, paths ...PlanPathSpec) ([][]Values, error)
- type Path
- type PathOption
- type Plan
- type PlanEntry
- type PlanOption
- type PlanPathSpec
- type Step
- func AnyExpand(md protoreflect.MessageDescriptor) Step
- func FieldAccess(fd protoreflect.FieldDescriptor) Step
- func ListIndex(i int) Step
- func ListRange(start, end int) Step
- func ListRangeFrom(start int) Step
- func ListRangeStep3(start, end, step int, startOmitted, endOmitted bool) Step
- func ListWildcard() Step
- func MapIndex(k protoreflect.MapKey) Step
- func Root(md protoreflect.MessageDescriptor) Step
- func (s Step) EndOmitted() bool
- func (s Step) FieldDescriptor() protoreflect.FieldDescriptor
- func (s Step) Kind() StepKind
- func (s Step) ListIndex() int
- func (s Step) MapIndex() protoreflect.MapKey
- func (s Step) MessageDescriptor() protoreflect.MessageDescriptor
- func (s Step) ProtoStep() protopath.Step
- func (s Step) RangeEnd() int
- func (s Step) RangeOpen() bool
- func (s Step) RangeStart() int
- func (s Step) RangeStep() int
- func (s Step) StartOmitted() bool
- type StepKind
- type Values
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func FormatValue ¶
func FormatValue(v protoreflect.Value, fd protoreflect.FieldDescriptor) string
FormatValue returns a human-readable string representation of a protoreflect.Value.
func PathValuesMulti ¶
func PathValuesMulti(md protoreflect.MessageDescriptor, m proto.Message, paths ...PlanPathSpec) ([][]Values, error)
PathValuesMulti is a convenience wrapper that compiles a Plan from the given path specs and immediately evaluates it against msg. For repeated evaluation of the same paths against many messages, prefer NewPlan + Plan.Eval.
Example ¶
ExamplePathValuesMulti demonstrates the convenience wrapper for one-shot multi-path evaluation.
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
"github.com/loicalleyne/bufarrowlib/proto/pbpath"
)
// exampleDescriptors constructs a Test schema with a Nested submessage and a
// repeated Test field called "repeats".
func exampleDescriptors() (protoreflect.MessageDescriptor, protoreflect.MessageDescriptor) {
stringType := descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()
messageType := descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum()
labelOptional := descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
labelRepeated := descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum()
fdp := &descriptorpb.FileDescriptorProto{
Name: proto.String("example.proto"),
Package: proto.String("example"),
Syntax: proto.String("proto3"),
MessageType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Test"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("nested"), Number: proto.Int32(1), Type: messageType, TypeName: proto.String(".example.Test.Nested"), Label: labelOptional},
{Name: proto.String("repeats"), Number: proto.Int32(2), Type: messageType, TypeName: proto.String(".example.Test"), Label: labelRepeated},
},
NestedType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Nested"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("stringfield"), Number: proto.Int32(1), Type: stringType, Label: labelOptional},
},
},
},
},
},
}
fd, err := protodesc.NewFile(fdp, nil)
if err != nil {
log.Fatalf("protodesc.NewFile: %v", err)
}
testMD := fd.Messages().ByName("Test")
nestedMD := testMD.Messages().ByName("Nested")
return testMD, nestedMD
}
func main() {
testMD, nestedMD := exampleDescriptors()
nested := dynamicpb.NewMessage(nestedMD)
nested.Set(nestedMD.Fields().ByName("stringfield"), protoreflect.ValueOfString("hello"))
msg := dynamicpb.NewMessage(testMD)
msg.Set(testMD.Fields().ByName("nested"), protoreflect.ValueOfMessage(nested))
results, err := pbpath.PathValuesMulti(testMD, msg,
pbpath.PlanPath("nested.stringfield", pbpath.Alias("greeting")),
)
if err != nil {
log.Fatal(err)
}
fmt.Println(results[0][0].Index(-1).Value.Interface())
}
Output: hello
Types ¶
type Path ¶
type Path []Step
Path is a sequence of protobuf reflection steps applied to some root protobuf message value to arrive at the current value. The first step must be a Root step.
func ParsePath ¶
func ParsePath(md protoreflect.MessageDescriptor, path string) (Path, error)
ParsePath translates a human-readable representation of a path into a Path.
An empty path is an empty string. A field access step is path '.' identifier A map index step is path '[' natural ']' A list index step is path '[' integer ']' (negative indices allowed) A list range step is path '[' start ':' end ']' or '[' start ':' ']' A list slice step is path '[' start ':' end ':' step ']' (Python-style slice)
- Any of start, end, step may be omitted: [::2], [1::], [::-1], [::]
- step=0 is an error.
- Both [:] and [::] produce a wildcard.
A list wildcard step is path '[' '*' ']' or '[' ':' ']' or '[' '::' ']' A root step is '(' msg.Descriptor().String() ')'
If the path does not start with '(' then the root step is implicitly for the given message. The parser is "type aware" to distinguish lists and maps keyed by numbers.
type PathOption ¶
type PathOption func(*pathOpts)
PathOption configures the behaviour of PathValues.
func Strict ¶
func Strict() PathOption
Strict returns a PathOption that makes PathValues return an error when a negative index or range bound resolves to an out-of-bounds position. Without Strict, out-of-bounds accesses are silently clamped or skipped.
type Plan ¶
type Plan struct {
// contains filtered or unexported fields
}
Plan is an immutable, pre-compiled bundle of Path values ready for repeated evaluation against messages of a single type.
Paths that share a common prefix are traversed once through the shared segment, then forked — giving an O(1) cost per shared step rather than O(P) where P is the number of paths.
A Plan is safe for concurrent use by multiple goroutines.
func NewPlan ¶
func NewPlan(md protoreflect.MessageDescriptor, paths ...PlanPathSpec) (*Plan, error)
NewPlan compiles one or more path strings against md into an immutable Plan. Parsing and trie construction happen once; Plan.Eval is the hot path.
All paths must be rooted at the same message descriptor md. Returns an error that bundles all parse failures.
Example ¶
ExampleNewPlan demonstrates compiling multiple paths into a Plan for repeated evaluation against messages of the same type.
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
"github.com/loicalleyne/bufarrowlib/proto/pbpath"
)
// exampleDescriptors constructs a Test schema with a Nested submessage and a
// repeated Test field called "repeats".
func exampleDescriptors() (protoreflect.MessageDescriptor, protoreflect.MessageDescriptor) {
stringType := descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()
messageType := descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum()
labelOptional := descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
labelRepeated := descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum()
fdp := &descriptorpb.FileDescriptorProto{
Name: proto.String("example.proto"),
Package: proto.String("example"),
Syntax: proto.String("proto3"),
MessageType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Test"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("nested"), Number: proto.Int32(1), Type: messageType, TypeName: proto.String(".example.Test.Nested"), Label: labelOptional},
{Name: proto.String("repeats"), Number: proto.Int32(2), Type: messageType, TypeName: proto.String(".example.Test"), Label: labelRepeated},
},
NestedType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Nested"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("stringfield"), Number: proto.Int32(1), Type: stringType, Label: labelOptional},
},
},
},
},
},
}
fd, err := protodesc.NewFile(fdp, nil)
if err != nil {
log.Fatalf("protodesc.NewFile: %v", err)
}
testMD := fd.Messages().ByName("Test")
nestedMD := testMD.Messages().ByName("Nested")
return testMD, nestedMD
}
func main() {
testMD, nestedMD := exampleDescriptors()
// Compile two paths — they share the "repeats[*].nested" prefix so
// the Plan traverses it only once.
plan, err := pbpath.NewPlan(testMD,
pbpath.PlanPath("repeats[*].nested.stringfield", pbpath.Alias("name")),
pbpath.PlanPath("nested.stringfield", pbpath.Alias("top")),
)
if err != nil {
log.Fatal(err)
}
// Build a message:
// Test { nested: { stringfield: "root" }, repeats: [
// Test{ nested: { stringfield: "a" } },
// Test{ nested: { stringfield: "b" } },
// ]}
nested := dynamicpb.NewMessage(nestedMD)
nested.Set(nestedMD.Fields().ByName("stringfield"), protoreflect.ValueOfString("root"))
msg := dynamicpb.NewMessage(testMD)
msg.Set(testMD.Fields().ByName("nested"), protoreflect.ValueOfMessage(nested))
list := msg.Mutable(testMD.Fields().ByName("repeats")).List()
for _, v := range []string{"a", "b"} {
n := dynamicpb.NewMessage(nestedMD)
n.Set(nestedMD.Fields().ByName("stringfield"), protoreflect.ValueOfString(v))
child := dynamicpb.NewMessage(testMD)
child.Set(testMD.Fields().ByName("nested"), protoreflect.ValueOfMessage(n))
list.Append(protoreflect.ValueOfMessage(child))
}
// Evaluate.
results, err := plan.Eval(msg)
if err != nil {
log.Fatal(err)
}
// results[0] = "name" path (repeats[*].nested.stringfield) → 2 branches
fmt.Printf("name: %d branches\n", len(results[0]))
for _, v := range results[0] {
fmt.Printf(" %v\n", v.Index(-1).Value.Interface())
}
// results[1] = "top" path (nested.stringfield) → 1 branch
fmt.Printf("top: %v\n", results[1][0].Index(-1).Value.Interface())
}
Output: name: 2 branches a b top: root
func (*Plan) Entries ¶
Entries returns metadata for each compiled path, in the order they were provided to NewPlan.
Example ¶
ExamplePlan_Entries shows how to inspect a Plan's compiled entries.
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"github.com/loicalleyne/bufarrowlib/proto/pbpath"
)
// exampleDescriptors constructs a Test schema with a Nested submessage and a
// repeated Test field called "repeats".
func exampleDescriptors() (protoreflect.MessageDescriptor, protoreflect.MessageDescriptor) {
stringType := descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()
messageType := descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum()
labelOptional := descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
labelRepeated := descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum()
fdp := &descriptorpb.FileDescriptorProto{
Name: proto.String("example.proto"),
Package: proto.String("example"),
Syntax: proto.String("proto3"),
MessageType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Test"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("nested"), Number: proto.Int32(1), Type: messageType, TypeName: proto.String(".example.Test.Nested"), Label: labelOptional},
{Name: proto.String("repeats"), Number: proto.Int32(2), Type: messageType, TypeName: proto.String(".example.Test"), Label: labelRepeated},
},
NestedType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Nested"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("stringfield"), Number: proto.Int32(1), Type: stringType, Label: labelOptional},
},
},
},
},
},
}
fd, err := protodesc.NewFile(fdp, nil)
if err != nil {
log.Fatalf("protodesc.NewFile: %v", err)
}
testMD := fd.Messages().ByName("Test")
nestedMD := testMD.Messages().ByName("Nested")
return testMD, nestedMD
}
func main() {
testMD, _ := exampleDescriptors()
plan, err := pbpath.NewPlan(testMD,
pbpath.PlanPath("nested.stringfield", pbpath.Alias("col_a")),
pbpath.PlanPath("repeats[*].nested.stringfield"),
)
if err != nil {
log.Fatal(err)
}
for _, e := range plan.Entries() {
fmt.Printf("%-30s path: %s\n", e.Name, e.Path)
}
}
Output: col_a path: (example.Test).nested.stringfield repeats[*].nested.stringfield path: (example.Test).repeats[*].nested.stringfield
func (*Plan) Eval ¶
Eval traverses msg along all compiled paths simultaneously. Paths sharing a prefix are traversed once through the shared segment.
Returns a slice of []Values indexed by entry position (matching the order paths were provided to NewPlan). Each []Values may contain multiple entries when the path fans out via wildcards, ranges, or slices.
Per-path StrictPath options are checked at leaf nodes: if any branch was clamped during traversal and the path is strict, an error is returned.
Example ¶
ExamplePlan_Eval shows how fan-out paths produce multiple Values branches.
package main
import (
"fmt"
"log"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/dynamicpb"
"github.com/loicalleyne/bufarrowlib/proto/pbpath"
)
// exampleDescriptors constructs a Test schema with a Nested submessage and a
// repeated Test field called "repeats".
func exampleDescriptors() (protoreflect.MessageDescriptor, protoreflect.MessageDescriptor) {
stringType := descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()
messageType := descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum()
labelOptional := descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum()
labelRepeated := descriptorpb.FieldDescriptorProto_LABEL_REPEATED.Enum()
fdp := &descriptorpb.FileDescriptorProto{
Name: proto.String("example.proto"),
Package: proto.String("example"),
Syntax: proto.String("proto3"),
MessageType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Test"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("nested"), Number: proto.Int32(1), Type: messageType, TypeName: proto.String(".example.Test.Nested"), Label: labelOptional},
{Name: proto.String("repeats"), Number: proto.Int32(2), Type: messageType, TypeName: proto.String(".example.Test"), Label: labelRepeated},
},
NestedType: []*descriptorpb.DescriptorProto{
{
Name: proto.String("Nested"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: proto.String("stringfield"), Number: proto.Int32(1), Type: stringType, Label: labelOptional},
},
},
},
},
},
}
fd, err := protodesc.NewFile(fdp, nil)
if err != nil {
log.Fatalf("protodesc.NewFile: %v", err)
}
testMD := fd.Messages().ByName("Test")
nestedMD := testMD.Messages().ByName("Nested")
return testMD, nestedMD
}
func main() {
testMD, nestedMD := exampleDescriptors()
plan, err := pbpath.NewPlan(testMD,
pbpath.PlanPath("repeats[0:2].nested.stringfield"),
)
if err != nil {
log.Fatal(err)
}
// Three-element repeated field — the [0:2] slice selects the first two.
msg := dynamicpb.NewMessage(testMD)
list := msg.Mutable(testMD.Fields().ByName("repeats")).List()
for _, v := range []string{"x", "y", "z"} {
n := dynamicpb.NewMessage(nestedMD)
n.Set(nestedMD.Fields().ByName("stringfield"), protoreflect.ValueOfString(v))
child := dynamicpb.NewMessage(testMD)
child.Set(testMD.Fields().ByName("nested"), protoreflect.ValueOfMessage(n))
list.Append(protoreflect.ValueOfMessage(child))
}
results, err := plan.Eval(msg)
if err != nil {
log.Fatal(err)
}
for _, v := range results[0] {
fmt.Println(v.Index(-1).Value.Interface())
}
}
Output: x y
type PlanEntry ¶
type PlanEntry struct {
// Name is the alias if one was set via [Alias], otherwise the original
// path string.
Name string
// Path is the compiled [Path].
Path Path
}
PlanEntry exposes metadata about one compiled path inside a Plan.
type PlanOption ¶
type PlanOption func(*planEntryOpts)
PlanOption configures a single path entry inside a Plan.
func Alias ¶
func Alias(name string) PlanOption
Alias returns a PlanOption that gives this path entry a human-readable name. The alias is returned by Plan.Entries and is useful for mapping paths to output column names.
func StrictPath ¶
func StrictPath() PlanOption
StrictPath returns a PlanOption that makes this path's evaluation return an error when a range or index is clamped due to the list being shorter than the requested bound. Without StrictPath, out-of-bounds accesses are silently clamped or skipped.
type PlanPathSpec ¶
type PlanPathSpec struct {
// contains filtered or unexported fields
}
PlanPathSpec pairs a raw path string with per-path options. Create one with PlanPath.
func PlanPath ¶
func PlanPath(path string, opts ...PlanOption) PlanPathSpec
PlanPath creates a PlanPathSpec pairing a path string with per-path options.
type Step ¶
type Step struct {
// contains filtered or unexported fields
}
Step is a single operation in a Path. It wraps a protopath.Step for the standard step kinds and adds ListRangeStep and ListWildcardStep.
func AnyExpand ¶
func AnyExpand(md protoreflect.MessageDescriptor) Step
AnyExpand returns an AnyExpandStep for the given message descriptor.
func FieldAccess ¶
func FieldAccess(fd protoreflect.FieldDescriptor) Step
FieldAccess returns a FieldAccessStep for the given field descriptor.
func ListIndex ¶
ListIndex returns a ListIndexStep for the given index. Negative indices are allowed and resolved at traversal time. For non-negative indices, the underlying protopath.ListIndex is used. For negative indices, only the raw value is stored (since protopath panics on negatives).
func ListRange ¶
ListRange returns a ListRangeStep representing the half-open range [start, end) with stride 1. Both start and end may be negative (resolved at traversal time).
func ListRangeFrom ¶
ListRangeFrom returns a ListRangeStep with only a start bound (open end) and stride 1. This represents [start:] syntax.
func ListRangeStep3 ¶
ListRangeStep3 returns a ListRangeStep with explicit start, end, and step. Panics if step is 0.
Use startOmitted/endOmitted to indicate that the respective bound should be defaulted at traversal time based on the step sign (Python semantics).
func ListWildcard ¶
func ListWildcard() Step
ListWildcard returns a ListWildcardStep that selects every element.
func MapIndex ¶
func MapIndex(k protoreflect.MapKey) Step
MapIndex returns a MapIndexStep for the given map key.
func Root ¶
func Root(md protoreflect.MessageDescriptor) Step
Root returns a RootStep for the given message descriptor.
func (Step) EndOmitted ¶
EndOmitted reports whether the end bound was omitted in the source syntax. When true, the effective end is computed at traversal time based on the step sign: len for positive step, -(len+1) for negative step.
func (Step) FieldDescriptor ¶
func (s Step) FieldDescriptor() protoreflect.FieldDescriptor
FieldDescriptor returns the field descriptor for a FieldAccessStep.
func (Step) ListIndex ¶
ListIndex returns the list index for a ListIndexStep. May be negative (resolved at traversal time).
func (Step) MapIndex ¶
func (s Step) MapIndex() protoreflect.MapKey
MapIndex returns the map key for a MapIndexStep.
func (Step) MessageDescriptor ¶
func (s Step) MessageDescriptor() protoreflect.MessageDescriptor
MessageDescriptor returns the message descriptor for RootStep and AnyExpandStep.
func (Step) ProtoStep ¶
ProtoStep returns the underlying protopath.Step. It panics if the step kind is ListRangeStep or ListWildcardStep.
func (Step) RangeEnd ¶
RangeEnd returns the end bound of a ListRangeStep. The value is 0 when Step.EndOmitted is true.
func (Step) RangeOpen ¶
RangeOpen reports whether the range has no explicit end bound. Deprecated: use Step.EndOmitted instead.
func (Step) RangeStart ¶
RangeStart returns the start bound of a ListRangeStep. The value is 0 when Step.StartOmitted is true.
func (Step) RangeStep ¶
RangeStep returns the stride of a ListRangeStep. Defaults to 1 when not explicitly provided. Never 0.
func (Step) StartOmitted ¶
StartOmitted reports whether the start bound was omitted in the source syntax. When true, the effective start is computed at traversal time based on the step sign: 0 for positive step, len-1 for negative step.
type StepKind ¶
type StepKind int
StepKind enumerates the kinds of steps in a Path. The first six values mirror protopath.StepKind.
const ( // RootStep identifies a [Step] as a root message. RootStep StepKind = iota // FieldAccessStep identifies a [Step] as accessing a message field by name. FieldAccessStep // UnknownAccessStep identifies a [Step] as accessing unknown fields. UnknownAccessStep // ListIndexStep identifies a [Step] as indexing into a list by position. // Negative indices are allowed and resolved at traversal time. ListIndexStep // MapIndexStep identifies a [Step] as indexing into a map by key. MapIndexStep // AnyExpandStep identifies a [Step] as expanding an Any message. AnyExpandStep // ListRangeStep identifies a [Step] as a Python-style slice // [start:end:step] applied to a repeated field during value traversal. // Any of start, end, step may be omitted; omitted bounds are resolved // at traversal time based on the step sign (see Python slice semantics). ListRangeStep // ListWildcardStep identifies a [Step] as selecting all elements of a // repeated field. Equivalent to [*], [:], or [::] in the path syntax. ListWildcardStep )
type Values ¶
type Values struct {
Path Path
Values []protoreflect.Value
// contains filtered or unexported fields
}
Values is a Path paired with a sequence of values at each step. The lengths of [Values.Path] and [Values.Values] must be identical. The first step must be a Root step and the first value must be a concrete message value.
func PathValues ¶
PathValues returns the values along a path in message m.
When the path contains a ListWildcardStep or ListRangeStep the function fans out: one Values is produced for every matching list element (cartesian product when multiple fan-out steps are nested).
A single ListIndexStep with a negative index is resolved relative to the list length (e.g. -1 → last element).
func (Values) Index ¶
func (p Values) Index(i int) (out struct { Step Step Value protoreflect.Value }, )
Index returns the ith step and value and supports negative indexing. A negative index starts counting from the tail of the Values such that -1 refers to the last pair, -2 refers to the second-to-last pair, and so on.
func (Values) Len ¶
Len reports the length of the path and values. If the path and values have differing length, it returns the minimum length.
func (Values) ListIndices ¶
ListIndices returns the concrete list indices visited along this Values path. It collects the index from every ListIndexStep in order.