Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple Filter Subjects and Subject Validation #821

Merged
merged 9 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
93 changes: 48 additions & 45 deletions README.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion src/NATS.Client/Exceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ public sealed class ClientExDetail
public static readonly ClientExDetail JsSubOrderedNotAllowOnQueues = new ClientExDetail(Sub, 90018, "Ordered consumer not allowed on queues.");
public static readonly ClientExDetail JsSubPushCantHaveMaxBatch = new ClientExDetail(Sub, 90019, "Push subscriptions cannot supply max batch.");
public static readonly ClientExDetail JsSubPushCantHaveMaxBytes = new ClientExDetail(Sub, 90020, "Push subscriptions cannot supply max bytes.");
public static readonly ClientExDetail JsSubSubjectNeededToLookupStream = new ClientExDetail(Sub, 90022, "Subject needed to lookup stream. Provide either a subscribe subject or a ConsumerConfiguration filter subject.");

/* Not used in this client. */ // public static readonly ClientExDetail JsSubPushAsyncCantSetPending = new ClientExDetail(Sub, 90021, "Pending limits must be set directly on the dispatcher.");

public static readonly ClientExDetail JsSoDurableMismatch = new ClientExDetail(So, 90101, "Builder durable must match the consumer configuration durable if both are provided.");
Expand All @@ -259,6 +261,7 @@ public sealed class ClientExDetail
public static readonly ClientExDetail JsSoNameMismatch = new ClientExDetail(So, 90110, "Builder name must match the consumer configuration name if both are provided.");
public static readonly ClientExDetail JsSoOrderedMemStorageNotSuppliedOrTrue = new ClientExDetail(So, 90111, "Mem Storage must be true if supplied.");
public static readonly ClientExDetail JsSoOrderedReplicasNotSuppliedOrOne = new ClientExDetail(So, 90112, "Replicas must be 1 if supplied.");
public static readonly ClientExDetail JsSoNameOrDurableRequiredForBind = new ClientExDetail(So, 90113, "Name or Durable required for Bind.");

public static readonly ClientExDetail OsObjectNotFound = new ClientExDetail(Os, 90201, "The object was not found.");
public static readonly ClientExDetail OsObjectIsDeleted = new ClientExDetail(Os, 90202, "The object is deleted.");
Expand All @@ -272,7 +275,8 @@ public sealed class ClientExDetail

public static readonly ClientExDetail JsConsumerCreate290NotAvailable = new ClientExDetail(Con, 90301, "Name field not valid when v2.9.0 consumer create api is not available.");
public static readonly ClientExDetail JsConsumerNameDurableMismatch = new ClientExDetail(Con, 90302, "Name must match durable if both are supplied.");

public static readonly ClientExDetail JsMultipleFilterSubjects210NotAvailable = new ClientExDetail(Con, 90303, "Multiple filter subjects not available until server version 2.10.0.");

private const string Sub = "SUB";
private const string So = "SO";
private const string Os = "OS";
Expand Down
124 changes: 104 additions & 20 deletions src/NATS.Client/Internals/Validator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,30 +45,79 @@ internal static void Required<TKey, TValue>(IDictionary<TKey, TValue> d, string
}
}

internal static string ValidateSubject(string s, bool required)
{
return ValidateSubject(s, "Subject", required, false);
/*
cannot contain spaces \r \n \t
cannot start or end with subject token delimiter .
some things don't allow it to end greater
*/
public static string ValidateSubjectTerm(string subject, string label, bool required)
{
Tuple<bool, string> t = IsValidSubjectTerm(subject, label, required);
if (t.Item1)
{
return t.Item2;
}
throw new ArgumentException(t.Item2);
}

public static string ValidateSubject(string subject, string label, bool required, bool cantEndWithGt) {
if (EmptyAsNull(subject) == null) {

/*
* If is valid, tuple item1 is true and item2 is the subject
* If is not valid, tuple item1 is false and item2 is the error message
*/
internal static Tuple<bool, string> IsValidSubjectTerm(string subject, string label, bool required) {
subject = EmptyAsNull(subject);
if (subject == null) {
if (required) {
throw new ArgumentException($"{label} cannot be null or empty.");
return new Tuple<bool, string>(false, $"{label} cannot be null or empty.");
}
return null;
return new Tuple<bool, string>(true, null);
}
if (subject.EndsWith(".")) {
return new Tuple<bool, string>(false, $"{label} cannot end with '.'");
}

string[] segments = subject.Split('.');
for (int x = 0; x < segments.Length; x++) {
string segment = segments[x];
if (segment.Equals(">")) {
if (cantEndWithGt || x != segments.Length - 1) { // if it can end with gt, gt must be last segment
throw new ArgumentException(label + " cannot contain '>'");
mtmk marked this conversation as resolved.
Show resolved Hide resolved
for (int seg = 0; seg < segments.Length; seg++) {
string segment = segments[seg];
int sl = segment.Length;
if (sl == 0) {
if (seg == 0) {
return new Tuple<bool, string>(false, $"{label} cannot start with '.'");
}
return new Tuple<bool, string>(false, $"{label} segment cannot be empty");
}
else {
for (int m = 0; m < sl; m++) {
char c = segment[m];
switch (c) {
case ' ':
case '\r':
case '\n':
case '\t':
return new Tuple<bool, string>(false, $"{label} cannot contain space, tab, carriage return or linefeed character");
case '*':
case '>':
if (sl != 1) {
return new Tuple<bool, string>(false, $"{label} wildcard improperly placed.");
}
break;
}
}
}
else if (!segment.Equals("*") && NotPrintable(segment)) {
throw new ArgumentException(label + " must be printable characters only.");
}
}
return new Tuple<bool, string>(true, subject);
}

public static string ValidateSubject(string s, bool required)
{
return ValidateSubject(s, "Subject", required, false);
}

public static string ValidateSubject(string subject, string label, bool required, bool cantEndWithGt) {
subject = ValidateSubjectTerm(subject, label, required);
if (subject != null && cantEndWithGt && subject.EndsWith(".>")) {
throw new ArgumentException($"{label} last segment cannot be '>'");
}
return subject;
}

Expand Down Expand Up @@ -444,7 +493,7 @@ internal static long ValidateNotNegative(long l, string label) {
// ----------------------------------------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------------------------------------

[Obsolete("This property is obsolete. use string.IsNullOrWhiteSpace(string) instead.", false)]
public static bool NullOrEmpty(string s)
{
return string.IsNullOrWhiteSpace(s);
Expand Down Expand Up @@ -604,13 +653,28 @@ public static bool NotPrintableOrHasWildGtDollar(string s) {

public static string EmptyAsNull(string s)
{
return NullOrEmpty(s) ? null : s;
return string.IsNullOrWhiteSpace(s) ? null : s;
}

public static string EmptyOrNullAs(string s, string ifEmpty) {
return NullOrEmpty(s) ? ifEmpty : s;
return string.IsNullOrWhiteSpace(s) ? ifEmpty : s;
}

public static IList<TSource> EmptyAsNull<TSource>(IList<TSource> list)
{
return EmptyOrNull(list) ? null : list;
}

public static bool EmptyOrNull<TSource>(IList<TSource> list)
{
return list == null || list.Count == 0;
}

public static bool EmptyOrNull<TSource>(TSource[] list)
{
return list == null || list.Length == 0;
}

public static bool ZeroOrLtMinus1(long l)
{
return l == 0 || l < -1;
Expand Down Expand Up @@ -663,7 +727,7 @@ public static bool IsSemVer(string s)
return Regex.IsMatch(s, SemVerPattern);
}

public static bool SequenceEqual<TSource>(IList<TSource> l1, IList<TSource> l2, bool nullSecondEqualsEmptyFirst = true)
public static bool SequenceEqual<T>(IList<T> l1, IList<T> l2, bool nullSecondEqualsEmptyFirst = true)
{
if (l1 == null)
{
Expand All @@ -678,6 +742,26 @@ public static bool SequenceEqual<TSource>(IList<TSource> l1, IList<TSource> l2,
return l1.SequenceEqual(l2);
}

public static bool ListsAreEquivalent<T>(IList<T> l1, IList<T> l2)
{
if (l1 == null || l1.Count == 0)
{
return l2 == null || l2.Count == 0;
}

if (l2 == null || l1.Count != l2.Count)
{
return false;
}

foreach (T t in l1) {
if (!l2.Contains(t)) {
mtmk marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
}
return true;
}

public static bool DictionariesEqual(IDictionary<string, string> d1, IDictionary<string, string> d2)
{
if (d1 == d2)
Expand Down
1 change: 1 addition & 0 deletions src/NATS.Client/JetStream/ApiConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ internal static class ApiConstants
internal const string External = "external";
internal const string Filter = "filter";
internal const string FilterSubject = "filter_subject";
internal const string FilterSubjects = "filter_subjects";
internal const string FirstSequence = "first_seq";
internal const string FirstTs = "first_ts";
internal const string FlowControl = "flow_control";
Expand Down
Loading