From 916e0c8ad1193e24366f4c55409b3137a9d5f920 Mon Sep 17 00:00:00 2001 From: Haga Rak Date: Fri, 20 Mar 2026 14:35:25 +0100 Subject: [PATCH] Add IProtobufDecoder extension point for custom gRPC message decoding Allow consumers to provide their own protobuf decoding logic via FormatSettings.ProtobufDecoder, removing the hard dependency on the protoc CLI tool. The custom decoder is tried first, with protoc-based and raw wire-format decoding as fallbacks. --- src/Fluxzy.Core/Formatters/FormatSettings.cs | 8 ++ .../Formatters/IProtobufDecoder.cs | 98 +++++++++++++++++++ .../Producers/Grpc/RequestProtobufProducer.cs | 17 +++- .../Grpc/ResponseProtobufProducer.cs | 4 +- 4 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 src/Fluxzy.Core/Formatters/IProtobufDecoder.cs diff --git a/src/Fluxzy.Core/Formatters/FormatSettings.cs b/src/Fluxzy.Core/Formatters/FormatSettings.cs index b7f6fdc9..673aaaf3 100644 --- a/src/Fluxzy.Core/Formatters/FormatSettings.cs +++ b/src/Fluxzy.Core/Formatters/FormatSettings.cs @@ -20,6 +20,14 @@ public class FormatSettings public int MaxFormattableProtobufLength { get; set; } = 2 * 1024 * 1024; + /// + /// An optional custom protobuf decoder. When set, this decoder is tried first for + /// gRPC message decoding. If it returns null, the built-in protoc-based decoding + /// is used as a fallback (when is configured and + /// protoc is available on PATH), followed by raw wire-format decoding. + /// + public IProtobufDecoder? ProtobufDecoder { get; set; } + public List ProtoDirectories { get; set; } = new(); } } diff --git a/src/Fluxzy.Core/Formatters/IProtobufDecoder.cs b/src/Fluxzy.Core/Formatters/IProtobufDecoder.cs new file mode 100644 index 00000000..af8aa678 --- /dev/null +++ b/src/Fluxzy.Core/Formatters/IProtobufDecoder.cs @@ -0,0 +1,98 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; + +namespace Fluxzy.Formatters +{ + /// + /// Decodes raw protobuf message bytes into a human-readable text representation. + /// Implement this interface to provide custom protobuf decoding + /// without requiring the protoc CLI tool on PATH. + /// + /// + /// Using a delegate: + /// + /// formatSettings.ProtobufDecoder = ProtobufDecoder.Create(context => { + /// // Your custom decoding logic here + /// return decodedText; + /// }); + /// + /// + public interface IProtobufDecoder + { + /// + /// Attempts to decode a protobuf message into a human-readable text representation. + /// + /// Context containing the raw message bytes and gRPC metadata. + /// A decoded text representation of the message, or null if decoding is not possible. + string? TryDecode(ProtobufDecodeContext context); + } + + /// + /// Provides context for decoding a single protobuf message extracted from a gRPC frame. + /// + public readonly struct ProtobufDecodeContext + { + public ProtobufDecodeContext( + ReadOnlyMemory messageData, + string? serviceName, + string? methodName, + bool isRequest) + { + MessageData = messageData; + ServiceName = serviceName; + MethodName = methodName; + IsRequest = isRequest; + } + + /// + /// The raw protobuf message bytes (without the gRPC 5-byte frame header). + /// + public ReadOnlyMemory MessageData { get; } + + /// + /// The gRPC service name extracted from the request path (e.g., "mypackage.MyService"). + /// + public string? ServiceName { get; } + + /// + /// The gRPC method name extracted from the request path (e.g., "MyMethod"). + /// + public string? MethodName { get; } + + /// + /// True if this is a request (input) message, false for a response (output) message. + /// + public bool IsRequest { get; } + } + + /// + /// Factory methods for creating instances. + /// + public static class ProtobufDecoder + { + /// + /// Creates an from a delegate. + /// + /// + /// A function that takes a and returns + /// a decoded text representation, or null if decoding is not possible. + /// + public static IProtobufDecoder Create(Func decode) + { + return new DelegateProtobufDecoder(decode ?? throw new ArgumentNullException(nameof(decode))); + } + + private class DelegateProtobufDecoder : IProtobufDecoder + { + private readonly Func _decode; + + public DelegateProtobufDecoder(Func decode) + { + _decode = decode; + } + + public string? TryDecode(ProtobufDecodeContext context) => _decode(context); + } + } +} diff --git a/src/Fluxzy.Core/Formatters/Producers/Grpc/RequestProtobufProducer.cs b/src/Fluxzy.Core/Formatters/Producers/Grpc/RequestProtobufProducer.cs index 981188e0..22c6c52d 100644 --- a/src/Fluxzy.Core/Formatters/Producers/Grpc/RequestProtobufProducer.cs +++ b/src/Fluxzy.Core/Formatters/Producers/Grpc/RequestProtobufProducer.cs @@ -46,8 +46,8 @@ internal class RequestProtobufProducer : IFormattingProducer data, ProtoFileRegistry? registry, + IProtobufDecoder? customDecoder, string? serviceName, string? methodName, bool isRequest, @@ -78,6 +79,18 @@ internal static string TryDecodeFrame( { usedDescriptor = false; + // Try custom decoder first + if (customDecoder != null) { + var context = new ProtobufDecodeContext(data, serviceName, methodName, isRequest); + var result = customDecoder.TryDecode(context); + + if (result != null) { + usedDescriptor = true; + return result; + } + } + + // Fall back to protoc-based decoding if (registry != null && serviceName != null && methodName != null) { var (input, output) = registry.FindServiceMethod(serviceName, methodName); var descriptor = isRequest ? input : output; diff --git a/src/Fluxzy.Core/Formatters/Producers/Grpc/ResponseProtobufProducer.cs b/src/Fluxzy.Core/Formatters/Producers/Grpc/ResponseProtobufProducer.cs index ec6e356c..b6b93d66 100644 --- a/src/Fluxzy.Core/Formatters/Producers/Grpc/ResponseProtobufProducer.cs +++ b/src/Fluxzy.Core/Formatters/Producers/Grpc/ResponseProtobufProducer.cs @@ -51,8 +51,8 @@ internal class ResponseProtobufProducer : IFormattingProducer