Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/Fluxzy.Core/Formatters/FormatSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ public class FormatSettings

public int MaxFormattableProtobufLength { get; set; } = 2 * 1024 * 1024;

/// <summary>
/// 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 <see cref="ProtoDirectories" /> is configured and
/// protoc is available on PATH), followed by raw wire-format decoding.
/// </summary>
public IProtobufDecoder? ProtobufDecoder { get; set; }

public List<string> ProtoDirectories { get; set; } = new();
}
}
98 changes: 98 additions & 0 deletions src/Fluxzy.Core/Formatters/IProtobufDecoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak

using System;

namespace Fluxzy.Formatters
{
/// <summary>
/// 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.
/// </summary>
/// <example>
/// Using a delegate:
/// <code>
/// formatSettings.ProtobufDecoder = ProtobufDecoder.Create(context => {
/// // Your custom decoding logic here
/// return decodedText;
/// });
/// </code>
/// </example>
public interface IProtobufDecoder
{
/// <summary>
/// Attempts to decode a protobuf message into a human-readable text representation.
/// </summary>
/// <param name="context">Context containing the raw message bytes and gRPC metadata.</param>
/// <returns>A decoded text representation of the message, or null if decoding is not possible.</returns>
string? TryDecode(ProtobufDecodeContext context);
}

/// <summary>
/// Provides context for decoding a single protobuf message extracted from a gRPC frame.
/// </summary>
public readonly struct ProtobufDecodeContext
{
public ProtobufDecodeContext(
ReadOnlyMemory<byte> messageData,
string? serviceName,
string? methodName,
bool isRequest)
{
MessageData = messageData;
ServiceName = serviceName;
MethodName = methodName;
IsRequest = isRequest;
}

/// <summary>
/// The raw protobuf message bytes (without the gRPC 5-byte frame header).
/// </summary>
public ReadOnlyMemory<byte> MessageData { get; }

/// <summary>
/// The gRPC service name extracted from the request path (e.g., "mypackage.MyService").
/// </summary>
public string? ServiceName { get; }

/// <summary>
/// The gRPC method name extracted from the request path (e.g., "MyMethod").
/// </summary>
public string? MethodName { get; }

/// <summary>
/// True if this is a request (input) message, false for a response (output) message.
/// </summary>
public bool IsRequest { get; }
}

/// <summary>
/// Factory methods for creating <see cref="IProtobufDecoder" /> instances.
/// </summary>
public static class ProtobufDecoder
{
/// <summary>
/// Creates an <see cref="IProtobufDecoder" /> from a delegate.
/// </summary>
/// <param name="decode">
/// A function that takes a <see cref="ProtobufDecodeContext" /> and returns
/// a decoded text representation, or null if decoding is not possible.
/// </param>
public static IProtobufDecoder Create(Func<ProtobufDecodeContext, string?> decode)
{
return new DelegateProtobufDecoder(decode ?? throw new ArgumentNullException(nameof(decode)));
}

private class DelegateProtobufDecoder : IProtobufDecoder
{
private readonly Func<ProtobufDecodeContext, string?> _decode;

public DelegateProtobufDecoder(Func<ProtobufDecodeContext, string?> decode)
{
_decode = decode;
}

public string? TryDecode(ProtobufDecodeContext context) => _decode(context);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ internal class RequestProtobufProducer : IFormattingProducer<ProtobufFormattingR
continue;
}

var decoded = TryDecodeFrame(frame.Data, registry, serviceName, methodName,
true, maxLength, out var usedDescriptor);
var decoded = TryDecodeFrame(frame.Data, registry, context.Settings.ProtobufDecoder,
serviceName, methodName, true, maxLength, out var usedDescriptor);

if (usedDescriptor)
hasDescriptor = true;
Expand All @@ -70,6 +70,7 @@ internal class RequestProtobufProducer : IFormattingProducer<ProtobufFormattingR
internal static string TryDecodeFrame(
ReadOnlyMemory<byte> data,
ProtoFileRegistry? registry,
IProtobufDecoder? customDecoder,
string? serviceName,
string? methodName,
bool isRequest,
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ internal class ResponseProtobufProducer : IFormattingProducer<ProtobufFormatting
}

var decoded = RequestProtobufProducer.TryDecodeFrame(
frame.Data, registry, serviceName, methodName,
false, maxLength, out var usedDescriptor);
frame.Data, registry, context.Settings.ProtobufDecoder,
serviceName, methodName, false, maxLength, out var usedDescriptor);

if (usedDescriptor)
hasDescriptor = true;
Expand Down
Loading