Problem
extern_path only matches at the proto package prefix level. A mapping for a specific type FQN is silently ignored.
This came up while working with Buf on the BSR Cargo SDK plugins. BSR's SDK assembly injects extern_path mappings to point a downstream plugin's generated code at the upstream module's SDK crate. Module-level (package-prefix) injection works today, but per-type injection — which prost/tonic support and which BSR could need if a single proto package's files were split across more than one BSR module — does not.
Where the limitation lives
CodeGenContext::new (buffa-codegen/src/context.rs) resolves a single rust_module per file by matching the file's package against extern_paths, then registers every top-level type in that file under that one prefix:
// per file
let rust_module =
if let Some(rust_root) = resolve_extern_prefix(package, effective_extern_paths) {
rust_root
} else {
package.replace('.', "::")
};
// every top-level type in the file
let rust_path = format!("{}::{}", rust_module, name);
type_map.insert(fqn, rust_path);
resolve_extern_prefix (context.rs:637) takes only the package, never a type FQN:
fn resolve_extern_prefix(package: &str, extern_paths: &[(String, String)]) -> Option<String> {
let dotted = format!(".{}", package);
// longest-prefix match against extern_paths, requiring a `.` boundary
...
}
So extern_path=.google.protobuf.Timestamp=::other_crate::Ts never matches anything: the file's package is google.protobuf, and .google.protobuf does not have .google.protobuf.Timestamp as a prefix. The mapping is dead with no diagnostic.
How prost differs
prost resolves extern_path at type-resolution time against the type's FQN, exact-match first, then longest dotted prefix (prost-build/src/extern_paths.rs::resolve_ident):
pub fn resolve_ident(&self, pb_ident: &str) -> Option<String> {
if let Some(rust_path) = self.extern_paths.get(pb_ident) {
return Some(rust_path.clone()); // exact: per-type override
}
for (idx, _) in pb_ident.rmatch_indices('.') {
if let Some(rust_path) = self.extern_paths.get(&pb_ident[..idx]) { ... } // prefix fallback
}
None
}
extern_path=.google.protobuf.Timestamp=::pbjson_types::Timestamp is a normal thing to do in a prost build. Users migrating from prost will reach for this and find it silently does nothing here.
Proposed direction
Move extern_path matching from file-registration to type-registration in CodeGenContext::new: when registering a type FQN, first check for an exact (or longest-FQN-prefix) match in extern_paths, falling back to the file's package-level resolution if none. Same trie-style longest-match prost uses.
The non-trivial part is the __buffa:: ancillary tree. View/oneof/extension paths are derived as <package_module>::__buffa::view::<TypeView> etc., so we need to know where the package boundary falls inside a per-type Rust path. With package-level extern_path the boundary is the mapping. With per-type (.foo.bar.Outer.Inner → ::ext::foo::bar::outer::Inner), the boundary has to be recovered: count nesting segments past the package using package_of[fqn] and split the supplied Rust path at that point. That should be where the bugs hide; needs targeted tests for nested types and enums under a per-type override.
Independent of the implementation, a good first step is a diagnostic: warn (or error) when an extern_path entry's proto path isn't a package-only prefix, since today it's a silent no-op.
Related
- The package-only behavior was sufficient for the WKT auto-injection (
.google.protobuf → ::buffa_types::google::protobuf) and for buffa_module= / extern_path=.=… catch-alls, which is why this hasn't surfaced before.
connectrpc-codegen's TypeResolver wraps CodeGenContext::for_generate() for zero-drift type resolution, so it inherits whatever buffa-codegen does here. Tracking issue filed there as well.
Problem
extern_pathonly matches at the proto package prefix level. A mapping for a specific type FQN is silently ignored.This came up while working with Buf on the BSR Cargo SDK plugins. BSR's SDK assembly injects
extern_pathmappings to point a downstream plugin's generated code at the upstream module's SDK crate. Module-level (package-prefix) injection works today, but per-type injection — which prost/tonic support and which BSR could need if a single proto package's files were split across more than one BSR module — does not.Where the limitation lives
CodeGenContext::new(buffa-codegen/src/context.rs) resolves a singlerust_moduleper file by matching the file'spackageagainstextern_paths, then registers every top-level type in that file under that one prefix:resolve_extern_prefix(context.rs:637) takes only the package, never a type FQN:So
extern_path=.google.protobuf.Timestamp=::other_crate::Tsnever matches anything: the file'spackageisgoogle.protobuf, and.google.protobufdoes not have.google.protobuf.Timestampas a prefix. The mapping is dead with no diagnostic.How prost differs
prost resolves
extern_pathat type-resolution time against the type's FQN, exact-match first, then longest dotted prefix (prost-build/src/extern_paths.rs::resolve_ident):extern_path=.google.protobuf.Timestamp=::pbjson_types::Timestampis a normal thing to do in a prost build. Users migrating from prost will reach for this and find it silently does nothing here.Proposed direction
Move extern_path matching from file-registration to type-registration in
CodeGenContext::new: when registering a type FQN, first check for an exact (or longest-FQN-prefix) match inextern_paths, falling back to the file's package-level resolution if none. Same trie-style longest-match prost uses.The non-trivial part is the
__buffa::ancillary tree. View/oneof/extension paths are derived as<package_module>::__buffa::view::<TypeView>etc., so we need to know where the package boundary falls inside a per-type Rust path. With package-level extern_path the boundary is the mapping. With per-type (.foo.bar.Outer.Inner→::ext::foo::bar::outer::Inner), the boundary has to be recovered: count nesting segments past the package usingpackage_of[fqn]and split the supplied Rust path at that point. That should be where the bugs hide; needs targeted tests for nested types and enums under a per-type override.Independent of the implementation, a good first step is a diagnostic: warn (or error) when an
extern_pathentry's proto path isn't a package-only prefix, since today it's a silent no-op.Related
.google.protobuf→::buffa_types::google::protobuf) and forbuffa_module=/extern_path=.=…catch-alls, which is why this hasn't surfaced before.connectrpc-codegen'sTypeResolverwrapsCodeGenContext::for_generate()for zero-drift type resolution, so it inherits whatever buffa-codegen does here. Tracking issue filed there as well.