client

package
v0.70.0 Latest Latest
Warning

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

Go to latest
Published: May 6, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package client provides a remote-backed protobuf descriptor resolver that fetches schemas from a running protoregistry server over gRPC.

A Resolver is bound to a single namespace and implements the standard protobuf-go reflection interfaces (protoregistry.MessageTypeResolver, protoregistry.ExtensionTypeResolver, protodesc.Resolver) so it composes with anything that takes a resolver: dynamicpb, protojson, anypb, and external codec libraries such as github.com/trendvidia/protowire-go's PXF and SBE encoders.

Concrete defaults

  • Eager population. New / Dial fetch every schema in the namespace up front. Lookup misses surface at startup, not in the request path.
  • Polling refresh. A background goroutine calls ListSchemas on a fixed interval (default 30s) and re-fetches descriptors only for schemas whose current version advanced. Hot-swaps are atomic; readers in flight see a consistent snapshot. Failures during refresh are logged and survived — callers see stale-but-consistent state until the next successful tick.
  • Fail-loud collisions. If two schemas in the namespace export the same fully-qualified type name, New returns an error rather than silently picking one.

These choices mirror the in-process resolve.Resolver semantics where possible. Streaming refresh, lazy population, and other strategies are out of scope for v0.

Example

Dial a registry, fetch a message descriptor by fully-qualified name, and use it to decode a PXF payload via protowire-go:

ctx := context.Background()
r, err := client.Dial(ctx, "registry.internal:50051", "billing")
if err != nil {
    log.Fatal(err)
}
defer r.Close()

desc, err := r.FindDescriptorByName("billing.v1.Config")
if err != nil {
    log.Fatal(err)
}

msg, err := pxf.UnmarshalDescriptor(pxfBytes, desc.(protoreflect.MessageDescriptor))
if err != nil {
    log.Fatal(err)
}
_ = msg

The Resolver also drops into protojson and anypb without adapter code:

opts := protojson.UnmarshalOptions{Resolver: r}
err := opts.Unmarshal(jsonBytes, msg)
Example

Example demonstrates the canonical wiring: Dial a registry, fetch a message descriptor by fully-qualified name, and decode a PXF payload against it via protowire-go.

The example compiles but is not executed (no // Output: directive), since it dials a server that is not running here. It serves as a godoc-rendered, vet-checked source of truth for the API shape.

package main

import (
	"context"
	"log"

	"google.golang.org/protobuf/reflect/protoreflect"

	"github.com/trendvidia/protoregistry/client"
	"github.com/trendvidia/protowire-go/encoding/pxf"
)

func main() {
	var pxfBytes []byte // payload produced elsewhere

	ctx := context.Background()
	r, err := client.Dial(ctx, "registry.internal:50051", "billing")
	if err != nil {
		log.Fatal(err)
	}
	defer r.Close()

	desc, err := r.FindDescriptorByName("billing.v1.Config")
	if err != nil {
		log.Fatal(err)
	}

	msg, err := pxf.UnmarshalDescriptor(pxfBytes, desc.(protoreflect.MessageDescriptor))
	if err != nil {
		log.Fatal(err)
	}
	_ = msg
}

Index

Examples

Constants

View Source
const DefaultRefreshInterval = 30 * time.Second

DefaultRefreshInterval is the cadence at which a Resolver polls the server for current-version changes when no explicit interval is set.

Variables

This section is empty.

Functions

This section is empty.

Types

type Option

type Option func(*config)

Option configures a Resolver at construction time.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets a structured logger for refresh activity, cache swaps, and stale-while-error events. Nil falls back to slog.Default; pass a discard logger to silence output.

func WithRefreshInterval

func WithRefreshInterval(d time.Duration) Option

WithRefreshInterval sets the polling cadence for current-version changes. Passing 0 disables refresh entirely (the Resolver becomes effectively pinned to its initial population).

Default: DefaultRefreshInterval.

func WithSchemas

func WithSchemas(ids ...string) Option

WithSchemas restricts the Resolver to a subset of schemas in the namespace. Useful when a service only consumes a known set of types and wants to skip fetching the rest.

When unset, the Resolver tracks every schema in the namespace.

func WithToken

func WithToken(token string) Option

WithToken attaches a bearer token to outgoing requests. Only honored by Dial; callers of New should configure auth on the *grpc.ClientConn directly (e.g. via grpc.WithPerRPCCredentials), which is more flexible and idiomatic.

type Resolver

type Resolver struct {
	// contains filtered or unexported fields
}

Resolver resolves protobuf descriptors for a single namespace from a remote protoregistry server.

It implements protoregistry.MessageTypeResolver, protoregistry.ExtensionTypeResolver, and the descriptor lookup half of protodesc.Resolver, so it drops into protojson, anypb, dynamicpb, and protowire-go without adapter code.

A Resolver is namespace-scoped to mirror the server model. Construct one Resolver per namespace.

func Dial

func Dial(ctx context.Context, addr, namespace string, opts ...Option) (*Resolver, error)

Dial is a convenience constructor that opens an insecure gRPC connection and constructs a Resolver in one call. Production callers should usually build a *grpc.ClientConn themselves and pass it to New.

func New

func New(ctx context.Context, conn *grpc.ClientConn, namespace string, opts ...Option) (*Resolver, error)

New constructs a Resolver bound to the given namespace on an already-dialed gRPC connection. The conn is owned by the caller, who is responsible for its lifecycle, transport credentials, interceptors, and observability hooks.

On success, the returned Resolver has eagerly populated descriptors for every schema in the namespace (or the subset selected via WithSchemas) and started its background refresh goroutine. Call Resolver.Close to stop it.

func (*Resolver) Close

func (r *Resolver) Close() error

Close stops the background refresh goroutine. If the Resolver was created via Dial it also closes the underlying gRPC connection; otherwise the conn was passed in by the caller and is left alone.

func (*Resolver) FindDescriptorByName

func (r *Resolver) FindDescriptorByName(name protoreflect.FullName) (protoreflect.Descriptor, error)

FindDescriptorByName looks up any descriptor (message, enum, service, extension, etc.) by its fully-qualified name.

func (*Resolver) FindExtensionByName

func (r *Resolver) FindExtensionByName(name protoreflect.FullName) (protoreflect.ExtensionType, error)

FindExtensionByName looks up an extension by its fully-qualified name.

func (*Resolver) FindExtensionByNumber

func (r *Resolver) FindExtensionByNumber(message protoreflect.FullName, field protoreflect.FieldNumber) (protoreflect.ExtensionType, error)

FindExtensionByNumber looks up an extension by the message it extends and its field number. Walks all schemas in the namespace.

func (*Resolver) FindFileByPath

func (r *Resolver) FindFileByPath(path string) (protoreflect.FileDescriptor, error)

FindFileByPath looks up a file descriptor by its proto path (e.g. "billing/v1/billing.proto"). Walks all schemas in the namespace.

func (*Resolver) FindMessageByName

func (r *Resolver) FindMessageByName(name protoreflect.FullName) (protoreflect.MessageType, error)

FindMessageByName looks up a message type by its fully-qualified name across all schemas in the namespace.

Returns protoregistry.NotFound when no schema in the namespace defines the name.

func (*Resolver) FindMessageByURL

func (r *Resolver) FindMessageByURL(url string) (protoreflect.MessageType, error)

FindMessageByURL looks up a message type by its type URL (e.g. "type.googleapis.com/billing.v1.Config"). Enables use with google.golang.org/protobuf/types/known/anypb.

func (*Resolver) Namespace

func (r *Resolver) Namespace() string

Namespace returns the namespace this Resolver is bound to.

func (*Resolver) NewMessage

func (r *Resolver) NewMessage(name protoreflect.FullName) (*dynamicpb.Message, error)

NewMessage constructs an empty dynamic message for the given fully-qualified name. Equivalent to looking up the descriptor and passing it to dynamicpb.NewMessage, but bundled into one call because callers almost always want the dynamic message, not the descriptor itself.

func (*Resolver) Pin

func (r *Resolver) Pin(ctx context.Context, versions map[string]uint64) (*Resolver, error)

Pin returns a derived Resolver frozen at the given (schemaID -> version) mapping. The parent Resolver is unaffected and continues to track current versions. Pinned Resolvers are intended for reproducible reads, e.g. replaying a captured PXF stream against the exact schema version it was produced with.

The returned Resolver shares the parent's gRPC connection. Closing the pinned Resolver does not affect the parent or the conn.

func (*Resolver) Refresh

func (r *Resolver) Refresh(ctx context.Context) error

Refresh forces a freshness check now, outside the regular polling cadence. Useful in tests and after a known publish/promote cycle.

Refresh is safe to call concurrently with itself and with the background refresh loop — calls are serialized internally. Lookups never block on Refresh; they read the snapshot atomically.

On error, the previous snapshot is preserved (stale-while-error).

func (*Resolver) Schema

func (r *Resolver) Schema(schemaID string) *SchemaResolver

Schema returns a SchemaResolver scoped to a single schema in the namespace. The returned resolver shares the parent's cache and refresh loop.

Example

ExampleResolver_Schema shows the SchemaResolver path, useful when the caller already knows which schema in the namespace owns the type. It's cheaper (skips the cross-schema name index) and immune to collisions across schemas.

package main

import (
	"context"
	"log"

	"github.com/trendvidia/protoregistry/client"
)

func main() {
	ctx := context.Background()
	r, err := client.Dial(ctx, "registry.internal:50051", "billing")
	if err != nil {
		log.Fatal(err)
	}
	defer r.Close()

	configSchema := r.Schema("config")
	msg, err := configSchema.NewMessage("billing.v1.Config")
	if err != nil {
		log.Fatal(err)
	}
	_ = msg
}

type SchemaResolver

type SchemaResolver struct {
	// contains filtered or unexported fields
}

SchemaResolver narrows lookups to a single schema within a namespace. Use it when the caller knows which schema owns a type — it skips the cross-schema name index and is unaffected by collisions across schemas.

func (*SchemaResolver) FindMessageByName

func (s *SchemaResolver) FindMessageByName(name protoreflect.FullName) (protoreflect.MessageType, error)

FindMessageByName looks up a message type within the bound schema.

func (*SchemaResolver) NewMessage

func (s *SchemaResolver) NewMessage(name protoreflect.FullName) (*dynamicpb.Message, error)

NewMessage constructs an empty dynamic message from the bound schema.

func (*SchemaResolver) SchemaID

func (s *SchemaResolver) SchemaID() string

SchemaID returns the schema this resolver is scoped to.

Directories

Path Synopsis
internal
clienttest
Package clienttest is a test-only harness that wires a real protoregistry server (Postgres + gRPC over bufconn) and exposes a *grpc.ClientConn ready to be passed to client.New, plus helpers for the publish/promote dance.
Package clienttest is a test-only harness that wires a real protoregistry server (Postgres + gRPC over bufconn) and exposes a *grpc.ClientConn ready to be passed to client.New, plus helpers for the publish/promote dance.

Jump to

Keyboard shortcuts

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