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
4 changes: 3 additions & 1 deletion eng/StackExchange.Redis.Build/AsciiHash.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ public static partial bool TryParse(ReadOnlySpan<byte> value, out SomeEnum value
Individual enum members can also be marked with `[AsciiHash("token value")]` to override the token payload. If
an enum member declares an empty explicit value (i.e. `[AsciiHash("")]`), then that member is ignored by the
tool; this is useful for marking "unknown" or "invalid" enum values (commonly the first enum, which by
convention has the value `0`):
convention has the value `0`). Note that this is about the *explicit* empty token: a member with no attribute
(or a bare `[AsciiHash]`) still infers its token from the member name, so a member that must not be matched
needs the explicit `[AsciiHash("")]`:

``` c#
public enum SomeEnum
Expand Down
24 changes: 23 additions & 1 deletion eng/StackExchange.Redis.Build/AsciiHashGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,34 @@ private static string GetRawValue(string name, AttributeData? asciiHashAttribute
}
if (string.IsNullOrWhiteSpace(value))
{
value = InferPayload(name); // if nothing explicit: infer from name
// an explicit empty token means "pretend you don't know about this" (usually the zero
// member, i.e. Unknown/None); anything else: infer from the name
value = HasExplicitToken(asciiHashAttribute) ? "" : InferPayload(name);
}

return value;
}

// the attribute's token parameter is optional, so a bare [AsciiHash] and [AsciiHash("")] are
// indistinguishable in ConstructorArguments (both report ""); the syntax tells them apart
private static bool HasExplicitToken(AttributeData? asciiHashAttribute)
{
if (asciiHashAttribute?.ApplicationSyntaxReference?.GetSyntax()
is not AttributeSyntax { ArgumentList.Arguments: { Count: > 0 } arguments })
{
return false;
}

foreach (var argument in arguments)
{
// NameEquals is a property assignment (i.e. CaseSensitive = false); anything else is
// the token, whether positional or named (i.e. token: "")
if (argument.NameEquals is null) return true;
}

return false;
}

private static string InferPayload(string name) => name.Replace("_", "-");

private (string Namespace, string ParentType, Accessibility Accessibility, string Name,
Expand Down
3 changes: 3 additions & 0 deletions src/RESPite/Shared/AsciiHash.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public sealed partial class AsciiHashAttribute(string token = "") : Attribute
/// The token expected when parsing data, if different from the implied value. The implied
/// value is the name, replacing underscores for hyphens, so: 'a_b' becomes 'a-b'.
/// </summary>
/// <remarks>An explicit empty token (i.e. <c>[AsciiHash("")]</c>) means that the member is
/// excluded, and cannot be parsed or formatted; this is for client-side values such as
/// <c>Unknown</c>. This is distinct from omitting the token, which infers it from the name.</remarks>
public string Token => token;

/// <summary>
Expand Down
6 changes: 2 additions & 4 deletions src/StackExchange.Redis/Enums/RedisCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ namespace StackExchange.Redis;
// ReSharper disable InconsistentNaming
internal enum RedisCommand
{
NONE, // must be first for "zero reasons"
[AsciiHash("")] // not a command; must be first for "zero reasons"
NONE,

APPEND,
ASKING,
Expand Down Expand Up @@ -317,9 +318,6 @@ internal static partial class RedisCommandMetadata

[AsciiHash(CaseSensitive = false)]
public static partial bool TryParseCI(ReadOnlySpan<char> command, out RedisCommand value);

[AsciiHash]
public static partial bool TryFormat(RedisCommand command, out string format);
}

// ReSharper restore InconsistentNaming
Expand Down
9 changes: 7 additions & 2 deletions src/StackExchange.Redis/Enums/RedisType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ public enum RedisType
/// <summary>
/// The specified key does not exist.
/// </summary>
[AsciiHash("")]
/// <remarks><c>none</c> is the literal reply from <c>TYPE</c> for a key that does not exist,
/// so this member has a real token (unlike <see cref="Unknown"/>).</remarks>
[AsciiHash("none")]
None,

/// <summary>
Expand Down Expand Up @@ -75,6 +77,8 @@ public enum RedisType
/// <summary>
/// The data-type was not recognised by the client library.
/// </summary>
/// <remarks>This is a client-side value, not a server token, so it is not parsable.</remarks>
[AsciiHash("")]
Unknown,

/// <summary>
Expand All @@ -96,7 +100,8 @@ public enum RedisType
/// </summary>
internal static partial class RedisTypeMetadata
{
[AsciiHash]
// TYPE replies are lower-case, but this is not a hot path, and v2 parsed case-insensitively
[AsciiHash(CaseSensitive = false)]
internal static partial bool TryParse(ReadOnlySpan<byte> value, out RedisType redisType);
}
}
21 changes: 21 additions & 0 deletions tests/StackExchange.Redis.Tests/KeyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,27 @@ public async Task FlushFetchRandomKey()
Assert.Equal(prefix + "abc", Encoding.UTF8.GetString(keyBytes));
}

[Fact]
public async Task KeyTypeOfMissingKeyIsNone() // see #3156
{
await using var conn = Create();

var db = conn.GetDatabase();
var key = Me();
db.KeyDelete(key, CommandFlags.FireAndForget);

Assert.Equal(RedisType.None, db.KeyType(key));
Assert.Equal(RedisType.None, await db.KeyTypeAsync(key));

db.StringSet(key, "abc", flags: CommandFlags.FireAndForget);
Assert.Equal(RedisType.String, db.KeyType(key));
Assert.Equal(RedisType.String, await db.KeyTypeAsync(key));

db.KeyDelete(key, CommandFlags.FireAndForget);
Assert.Equal(RedisType.None, db.KeyType(key));
Assert.Equal(RedisType.None, await db.KeyTypeAsync(key));
}

[Fact]
public async Task Zeros()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ public class BasicScalars(ITestOutputHelper log) : ResultProcessorUnitTest(log)
[InlineData("+set\r\n", Redis.RedisType.Set)]
[InlineData("+list\r\n", Redis.RedisType.List)]
[InlineData("+stream\r\n", Redis.RedisType.Stream)]
[InlineData("+vectorset\r\n", Redis.RedisType.VectorSet)]
[InlineData("+array\r\n", Redis.RedisType.Array)]
[InlineData("+none\r\n", Redis.RedisType.None)] // TYPE reply for a key that does not exist; see #3156
[InlineData("$4\r\nnone\r\n", Redis.RedisType.None)]
[InlineData("+blah\r\n", Redis.RedisType.Unknown)]
[InlineData("$-1\r\n", Redis.RedisType.None)]
[InlineData("_\r\n", Redis.RedisType.None)]
Expand Down
Loading