protoreflect icon indicating copy to clipboard operation
protoreflect copied to clipboard

desc/builder: feature request: auto-de-duplicate builders and already-built descriptors in transitive graph

Open kevin-break opened this issue 7 months ago • 4 comments

Hey, jhump:

First let me say this is a incredible complement to standard Go API v2, and I enjoyed a lot and thank you!

I want to construct a compact transitive closure from a "start" message, "compact" meaning removing all non-relevant symbols, even they appears in the original .proto file, which is always good for storage / transfer over network.

With the following code snippet, it complained

Received unexpected error:
    descriptors have cyclic dependency:...

I suspect when I tried to AddMessage (to the FileBuilder), as wrapped from protoreflect.MessageDescriptor, it drags the import path, and for some reason, some mis-registration happens in your package, but I don't know too much details to tell / fix.

Can you please help? Thanks.

--- Code snippet ---

// TransitiveClosure returns a compact set of related protobuf message,
// as the transitive closure of the desired protobuf message.
// All the symbols are packaged into a single FileDescriptorSet proto,
// to be consistent with common usage of delivering such dependency over network.
func TransitiveClosure[T proto.Message]() (*descriptorpb.FileDescriptorSet, error) {
	var zero T
	md := zero.ProtoReflect().Descriptor()

	// A map from the file name to the index (within fbs) for each FileBuilder instance.
	builderIdx := make(map[string]int)
	var fbs []*builder.FileBuilder

	knownSymbols := make(map[protoreflect.FullName]bool)
	queue := []protoreflect.Descriptor{md}
	for len(queue) > 0 {
		head := queue[0]
		queue = queue[1:]
		if knownSymbols[head.FullName()] {
			continue
		}
		if head.IsPlaceholder() { // Skip any missing files in the dependency graph.
			continue
		}

		pf := head.ParentFile()
		idx, found := builderIdx[pf.Path()]
		if !found { // We need another FileBuilder for a new .proto file in this transitive closure.
			idx = len(fbs)

			fbs = append(fbs, prepareFileBuilder(pf.Path(), string(pf.Package())))
			builderIdx[pf.Path()] = idx
		}
		knownSymbols[head.FullName()] = true

		switch descriptor := head.(type) {
		case protoreflect.MessageDescriptor:
			jmd, err := desc.WrapMessage(descriptor)
			if err != nil {
				return nil, err
			}
			mb, err := builder.FromMessage(jmd)
			if err != nil {
				return nil, err
			}
			fbs[idx].AddMessage(mb)

			fields := descriptor.Fields()
			for idx, len := 0, fields.Len(); idx < len; idx++ {
				switch f := fields.Get(idx); f.Kind() {
				case protoreflect.MessageKind:
					if f.IsMap() {
						if mv := f.MapValue(); mv.Kind() == protoreflect.MessageKind {
							queue = append(queue, mv.Message())
						}
						continue
					}
					queue = append(queue, f.Message())
				case protoreflect.EnumKind:
					queue = append(queue, f.Enum())
				}
			}
		case protoreflect.EnumDescriptor:
			jed, err := desc.WrapEnum(descriptor)
			if err != nil {
				return nil, err
			}
			eb, err := builder.FromEnum(jed)
			if err != nil {
				return nil, err
			}
			fbs[idx].AddEnum(eb)
		default:
			panic("unhandled?")
		}
	}

	var fds []*desc.FileDescriptor
	for idx := len(fbs) - 1; idx >= 0; idx-- {
		built, err := fbs[idx].Build()
		if err != nil {
			return nil, err
		}
		fds = append(fds, built)
	}
	return desc.ToFileDescriptorSet(fds...), nil
}

func prepareFileBuilder(path, packageName string) *builder.FileBuilder {
	fb := builder.NewFile(path)
	fb.SetProto3(true)
	fb.SetPackageName(packageName)
	return fb
}

kevin-break avatar Jun 25 '24 23:06 kevin-break