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
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<NatsVersion>2.7.1</NatsVersion>
<NatsVersion>2.7.2-preview.1</NatsVersion>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="NATS.Net" Version="$(NatsVersion)" />
Expand All @@ -10,6 +10,7 @@
<PackageVersion Include="NATS.Client.JetStream" Version="$(NatsVersion)" />
<PackageVersion Include="NATS.Client.Serializers.Json" Version="$(NatsVersion)" />
<PackageVersion Include="NATS.Client.ObjectStore" Version="$(NatsVersion)" />
<PackageVersion Include="NATS.Client.KeyValueStore" Version="$(NatsVersion)" />
<PackageVersion Include="NATS.Extensions.Microsoft.DependencyInjection" Version="$(NatsVersion)" />

<PackageVersion Include="System.Text.Json" Version="8.0.5"/>
Expand Down
2 changes: 2 additions & 0 deletions orbit.net.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
<File Path="src/Directory.Build.props" />
<Project Path="src/Synadia.Orbit.JetStream.Extensions/Synadia.Orbit.JetStream.Extensions.csproj" />
<Project Path="src/Synadia.Orbit.JetStream.Publisher/Synadia.Orbit.JetStream.Publisher.csproj" />
<Project Path="src/Synadia.Orbit.KeyValueStore.Extensions/Synadia.Orbit.KeyValueStore.Extensions.csproj" />
</Folder>
<Folder Name="/tests/">
<File Path="tests/Directory.Build.props" />
<Project Path="tests/Synadia.Orbit.JetStream.Extensions.Test/Synadia.Orbit.JetStream.Extensions.Test.csproj" />
<Project Path="tests/Synadia.Orbit.JetStream.Publisher.Test/Synadia.Orbit.JetStream.Publisher.Test.csproj" />
<Project Path="tests/Synadia.Orbit.KeyValueStore.Extensions.Test/Synadia.Orbit.KeyValueStore.Extensions.Test.csproj" />
<Project Path="tests/Synadia.Orbit.TestUtils/Synadia.Orbit.TestUtils.csproj" />
</Folder>
<Folder Name="/tools/">
Expand Down
40 changes: 40 additions & 0 deletions src/Synadia.Orbit.KeyValueStore.Extensions/Codecs/INatsKeyCodec.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Synadia Communications, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0.

namespace Synadia.Orbit.KeyValueStore.Extensions.Codecs;

/// <summary>
/// Defines the interface for encoding and decoding keys in a KV bucket.
/// </summary>
public interface INatsKeyCodec
{
/// <summary>
/// Encodes a key for storage.
/// </summary>
/// <param name="key">The key to encode.</param>
/// <returns>The encoded key.</returns>
string EncodeKey(string key);

/// <summary>
/// Decodes a key retrieved from storage.
/// </summary>
/// <param name="key">The encoded key to decode.</param>
/// <returns>The decoded key.</returns>
string DecodeKey(string key);
}

/// <summary>
/// An optional interface that key codecs can implement to support wildcard filtering operations.
/// If a key codec doesn't implement this interface, filter operations where the pattern contains
/// wildcards (* or &gt;) will throw <see cref="NatsKeyCodecException"/>.
/// </summary>
public interface INatsFilterableKeyCodec : INatsKeyCodec
{
/// <summary>
/// Encodes a pattern that may contain wildcards (* or &gt;).
/// Unlike <see cref="INatsKeyCodec.EncodeKey"/>, this must preserve wildcards in the result.
/// </summary>
/// <param name="filter">The filter pattern to encode.</param>
/// <returns>The encoded filter pattern with wildcards preserved.</returns>
string EncodeFilter(string filter);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) Synadia Communications, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0.

using System.Text;

namespace Synadia.Orbit.KeyValueStore.Extensions.Codecs;

/// <summary>
/// A codec that encodes keys using URL-safe Base64 encoding.
/// Each token (separated by '.') is encoded separately, preserving the NATS subject structure.
/// </summary>
public sealed class NatsBase64KeyCodec : INatsFilterableKeyCodec
{
private NatsBase64KeyCodec()
{
}

/// <summary>
/// Gets the singleton instance of the <see cref="NatsBase64KeyCodec"/>.
/// </summary>
public static NatsBase64KeyCodec Instance { get; } = new();

/// <inheritdoc/>
public string EncodeKey(string key)
{
var tokens = key.Split('.');
for (var i = 0; i < tokens.Length; i++)
{
tokens[i] = Base64UrlEncode(tokens[i]);
}

return string.Join(".", tokens);
}

/// <inheritdoc/>
public string DecodeKey(string key)
{
var tokens = key.Split('.');
for (var i = 0; i < tokens.Length; i++)
{
tokens[i] = Base64UrlDecode(tokens[i]);
}

return string.Join(".", tokens);
}

/// <inheritdoc/>
public string EncodeFilter(string filter)
{
var tokens = filter.Split('.');
for (var i = 0; i < tokens.Length; i++)
{
var token = tokens[i];
if (token != "*" && token != ">")
{
tokens[i] = Base64UrlEncode(token);
}
}

return string.Join(".", tokens);
}

private static string Base64UrlEncode(string input)
{
var bytes = Encoding.UTF8.GetBytes(input);
var base64 = Convert.ToBase64String(bytes);

// Convert to URL-safe Base64 (no padding)
return base64
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}

private static string Base64UrlDecode(string input)
{
// Convert from URL-safe Base64
var base64 = input
.Replace('-', '+')
.Replace('_', '/');

// Add padding if needed
switch (base64.Length % 4)
{
case 2:
base64 += "==";
break;
case 3:
base64 += "=";
break;
}

var bytes = Convert.FromBase64String(base64);
return Encoding.UTF8.GetString(bytes);
}
}
Loading
Loading