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