From 45f0daba2eec6b5ba1556bba5808a835272f0219 Mon Sep 17 00:00:00 2001 From: Scott Fauerbach Date: Tue, 26 Sep 2023 11:38:43 -0400 Subject: [PATCH] Subject Transforms in Mirror/Info and Source/Info (#820) --- src/NATS.Client/JetStream/ApiConstants.cs | 1 + src/NATS.Client/JetStream/Mirror.cs | 186 ++----------- src/NATS.Client/JetStream/Source.cs | 185 ++----------- src/NATS.Client/JetStream/SourceBase.cs | 254 ++++++++++++++++++ src/NATS.Client/JetStream/SourceInfoBase.cs | 5 + .../JetStream/StreamConfiguration.cs | 18 +- src/NATS.Client/JetStream/SubjectTransform.cs | 40 ++- .../UnitTests/Data/StreamConfiguration.json | 22 +- src/Tests/UnitTests/Data/StreamInfo.json | 30 ++- .../JetStream/TestStreamConfiguration.cs | 40 ++- .../UnitTests/JetStream/TestStreamInfo.cs | 26 +- 11 files changed, 434 insertions(+), 373 deletions(-) create mode 100644 src/NATS.Client/JetStream/SourceBase.cs diff --git a/src/NATS.Client/JetStream/ApiConstants.cs b/src/NATS.Client/JetStream/ApiConstants.cs index 8fd07f55d..dd14665ba 100644 --- a/src/NATS.Client/JetStream/ApiConstants.cs +++ b/src/NATS.Client/JetStream/ApiConstants.cs @@ -177,6 +177,7 @@ internal static class ApiConstants internal const string Streams = "streams"; internal const string Subject = "subject"; internal const string SubjectTransform = "subject_transform"; + internal const string SubjectTransforms = "subject_transforms"; internal const string Subjects = "subjects"; internal const string SubjectsFilter = "subjects_filter"; internal const string Success = "success"; diff --git a/src/NATS.Client/JetStream/Mirror.cs b/src/NATS.Client/JetStream/Mirror.cs index 02796a784..cf7340fb8 100644 --- a/src/NATS.Client/JetStream/Mirror.cs +++ b/src/NATS.Client/JetStream/Mirror.cs @@ -12,7 +12,7 @@ // limitations under the License. using System; -using NATS.Client.Internals; +using System.Collections.Generic; using NATS.Client.Internals.SimpleJSON; namespace NATS.Client.JetStream @@ -20,56 +20,8 @@ namespace NATS.Client.JetStream /// /// Information about a mirror. /// - public sealed class Mirror : JsonSerializable + public sealed class Mirror : SourceBase { - /// - /// Mirror stream name. - /// - public string Name { get; } - - /// - /// The sequence to start replicating from. - /// - public ulong StartSeq { get; } - - /// - /// The time stamp to start replicating from. - /// - public DateTime StartTime { get; } - - /// - /// The subject filter to replicate - /// - public string FilterSubject { get; } - - /// - /// External stream reference - /// - public External External { get; } - - internal Mirror(JSONNode mirrorBaseNode) - { - Name = mirrorBaseNode[ApiConstants.Name].Value; - StartSeq = mirrorBaseNode[ApiConstants.OptStartSeq].AsUlong; - StartTime = JsonUtils.AsDate(mirrorBaseNode[ApiConstants.OptStartTime]); - FilterSubject = mirrorBaseNode[ApiConstants.FilterSubject].Value; - External = External.OptionalInstance(mirrorBaseNode[ApiConstants.External]); - } - - public override JSONNode ToJsonNode() - { - JSONObject o = new JSONObject(); - JsonUtils.AddField(o, ApiConstants.Name, Name); - JsonUtils.AddField(o, ApiConstants.OptStartSeq, StartSeq); - JsonUtils.AddField(o, ApiConstants.OptStartTime, JsonUtils.ToString(StartTime)); - JsonUtils.AddField(o, ApiConstants.FilterSubject, FilterSubject); - if (External != null) - { - o[ApiConstants.External] = External.ToJsonNode(); - } - return o; - } - internal static Mirror OptionalInstance(JSONNode mirrorNode) { return mirrorNode == null || mirrorNode.Count == 0 ? null : new Mirror(mirrorNode); @@ -83,14 +35,14 @@ internal static Mirror OptionalInstance(JSONNode mirrorNode) /// the start time /// the filter subject /// the external reference - public Mirror(string name, ulong startSeq, DateTime startTime, string filterSubject, External external) - { - Name = name; - StartSeq = startSeq; - StartTime = startTime; - FilterSubject = filterSubject; - External = external; - } + /// the subject transforms, defaults to none + public Mirror(string name, ulong startSeq, DateTime startTime, string filterSubject, External external, + IList subjectTransforms = null) + : base(name, startSeq, startTime, filterSubject, external, subjectTransforms) {} + + internal Mirror(JSONNode mirrorNode) : base(mirrorNode) {} + + internal Mirror(MirrorBuilder mb) : base(mb) {} /// /// Creates a builder for a mirror object. @@ -111,126 +63,20 @@ public static MirrorBuilder Builder(Mirror mirror) { /// /// Mirror can be created using a MirrorBuilder. /// - public sealed class MirrorBuilder + public sealed class MirrorBuilder : SourceBaseBuilder { - private string _name; - private ulong _startSeq; - private DateTime _startTime; - private string _filterSubject; - private External _external; - - public MirrorBuilder() { } - - public MirrorBuilder(Mirror mirror) - { - _name = mirror.Name; - _startSeq = mirror.StartSeq; - _startTime = mirror.StartTime; - _filterSubject = mirror.FilterSubject; - _external = mirror.External; - } + public MirrorBuilder() {} + public MirrorBuilder(Mirror mirror) : base(mirror) {} - /// - /// Set the mirror name. - /// - /// the name - /// The Builder - public MirrorBuilder WithName(string name) + protected override MirrorBuilder GetThis() { - _name = name; return this; } - /// - /// Set the start sequence. - /// - /// the start sequence - /// The Builder - public MirrorBuilder WithStartSeq(ulong startSeq) + public override Mirror Build() { - _startSeq = startSeq; - return this; - } - - /// - /// Set the start time. - /// - /// the start time - /// The Builder - public MirrorBuilder WithStartTime(DateTime startTime) - { - _startTime = startTime; - return this; - } - - /// - /// Set the filter subject. - /// - /// the filterSubject - /// The Builder - public MirrorBuilder WithFilterSubject(string filterSubject) - { - _filterSubject = filterSubject; - return this; - } - - /// - /// Set the external reference. - /// - /// the external - /// The Builder - public MirrorBuilder WithExternal(External external) - { - _external = external; - return this; - } - - /// - /// Set the external reference by using a domain based prefix. - /// - /// the domain - /// The Builder - public MirrorBuilder WithDomain(string domain) - { - string prefix = JetStreamOptions.ConvertDomainToPrefix(domain); - _external = prefix == null ? null : External.Builder().WithApi(prefix).Build(); - return this; - } - - /// - /// Build a Mirror object - /// - /// The Mirror - public Mirror Build() - { - return new Mirror(_name, _startSeq, _startTime, _filterSubject, _external); + return new Mirror(this); } } - - public bool Equals(Mirror other) - { - return Name == other.Name && StartSeq == other.StartSeq && StartTime.Equals(other.StartTime) && FilterSubject == other.FilterSubject && Equals(External, other.External); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals((Mirror) obj); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = (Name != null ? Name.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ StartSeq.GetHashCode(); - hashCode = (hashCode * 397) ^ StartTime.GetHashCode(); - hashCode = (hashCode * 397) ^ (FilterSubject != null ? FilterSubject.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ (External != null ? External.GetHashCode() : 0); - return hashCode; - } - } } } diff --git a/src/NATS.Client/JetStream/Source.cs b/src/NATS.Client/JetStream/Source.cs index d69425207..c2821e15b 100644 --- a/src/NATS.Client/JetStream/Source.cs +++ b/src/NATS.Client/JetStream/Source.cs @@ -13,7 +13,6 @@ using System; using System.Collections.Generic; -using NATS.Client.Internals; using NATS.Client.Internals.SimpleJSON; namespace NATS.Client.JetStream @@ -21,56 +20,8 @@ namespace NATS.Client.JetStream /// /// Information about an upstream stream source in a mirror /// - public sealed class Source : JsonSerializable + public sealed class Source : SourceBase { - /// - /// Source stream name. - /// - public string Name { get; } - - /// - /// The sequence to start replicating from. - /// - public ulong StartSeq { get; } - - /// - /// The time stamp to start replicating from. - /// - public DateTime StartTime { get; } - - /// - /// The subject filter to replicate - /// - public string FilterSubject { get; } - - /// - /// External stream reference - /// - public External External { get; } - - internal Source(JSONNode sourceBaseNode) - { - Name = sourceBaseNode[ApiConstants.Name].Value; - StartSeq = sourceBaseNode[ApiConstants.OptStartSeq].AsUlong; - StartTime = JsonUtils.AsDate(sourceBaseNode[ApiConstants.OptStartTime]); - FilterSubject = sourceBaseNode[ApiConstants.FilterSubject].Value; - External = External.OptionalInstance(sourceBaseNode[ApiConstants.External]); - } - - public override JSONNode ToJsonNode() - { - JSONObject jso = new JSONObject(); - JsonUtils.AddField(jso, ApiConstants.Name, Name); - JsonUtils.AddField(jso, ApiConstants.OptStartSeq, StartSeq); - JsonUtils.AddField(jso, ApiConstants.OptStartTime, JsonUtils.ToString(StartTime)); - JsonUtils.AddField(jso, ApiConstants.FilterSubject, FilterSubject); - if (External != null) - { - jso[ApiConstants.External] = External.ToJsonNode(); - } - return jso; - } - internal static List OptionalListOf(JSONNode sourceListNode) { if (sourceListNode == null) @@ -94,14 +45,14 @@ internal static List OptionalListOf(JSONNode sourceListNode) /// the start time /// the filter subject /// the external reference - public Source(string name, ulong startSeq, DateTime startTime, string filterSubject, External external) - { - Name = name; - StartSeq = startSeq; - StartTime = startTime; - FilterSubject = filterSubject; - External = external; - } + /// the subject transforms, defaults to none + public Source(string name, ulong startSeq, DateTime startTime, string filterSubject, External external, + IList subjectTransforms = null) + : base(name, startSeq, startTime, filterSubject, external, subjectTransforms) {} + + internal Source(JSONNode sourceBaseNode) : base(sourceBaseNode) {} + + internal Source(SourceBuilder sb) : base(sb) {} /// /// Creates a builder for a source object. @@ -122,126 +73,20 @@ public static SourceBuilder Builder(Source source) { /// /// Source can be created using a SourceBuilder. /// - public sealed class SourceBuilder + public sealed class SourceBuilder : SourceBaseBuilder { - private string _name; - private ulong _startSeq; - private DateTime _startTime; - private string _filterSubject; - private External _external; - - public SourceBuilder() { } - - public SourceBuilder(Source source) - { - _name = source.Name; - _startSeq = source.StartSeq; - _startTime = source.StartTime; - _filterSubject = source.FilterSubject; - _external = source.External; - } + public SourceBuilder() {} + public SourceBuilder(Source source) : base(source) {} - /// - /// Set the source name. - /// - /// the name - /// The Builder - public SourceBuilder WithName(string name) + protected override SourceBuilder GetThis() { - _name = name; return this; } - /// - /// Set the start sequence. - /// - /// the start sequence - /// The Builder - public SourceBuilder WithStartSeq(ulong startSeq) + public override Source Build() { - _startSeq = startSeq; - return this; - } - - /// - /// Set the start time. - /// - /// the start time - /// The Builder - public SourceBuilder WithStartTime(DateTime startTime) - { - _startTime = startTime; - return this; - } - - /// - /// Set the filter subject. - /// - /// the filterSubject - /// The Builder - public SourceBuilder WithFilterSubject(string filterSubject) - { - _filterSubject = filterSubject; - return this; - } - - /// - /// Set the external reference. - /// - /// the external - /// The Builder - public SourceBuilder WithExternal(External external) - { - _external = external; - return this; - } - - /// - /// Set the external reference by using a domain based prefix. - /// - /// the domain - /// The Builder - public SourceBuilder WithDomain(string domain) - { - string prefix = JetStreamOptions.ConvertDomainToPrefix(domain); - _external = prefix == null ? null : External.Builder().WithApi(prefix).Build(); - return this; - } - - /// - /// Build a Source object - /// - /// The Source - public Source Build() - { - return new Source(_name, _startSeq, _startTime, _filterSubject, _external); + return new Source(this); } } - - public bool Equals(Source other) - { - return Name == other.Name && StartSeq == other.StartSeq && StartTime.Equals(other.StartTime) && FilterSubject == other.FilterSubject && Equals(External, other.External); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals((Source) obj); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = (Name != null ? Name.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ StartSeq.GetHashCode(); - hashCode = (hashCode * 397) ^ StartTime.GetHashCode(); - hashCode = (hashCode * 397) ^ (FilterSubject != null ? FilterSubject.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ (External != null ? External.GetHashCode() : 0); - return hashCode; - } - } } } diff --git a/src/NATS.Client/JetStream/SourceBase.cs b/src/NATS.Client/JetStream/SourceBase.cs new file mode 100644 index 000000000..cbfb8f9ed --- /dev/null +++ b/src/NATS.Client/JetStream/SourceBase.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using NATS.Client.Internals; +using NATS.Client.Internals.SimpleJSON; + +namespace NATS.Client.JetStream +{ + /// + /// Information about an upstream stream source or a mirror + /// + public class SourceBase : JsonSerializable + { + /// + /// Source stream name. + /// + public string Name { get; } + + /// + /// The sequence to start replicating from. + /// + public ulong StartSeq { get; } + + /// + /// The time stamp to start replicating from. + /// + public DateTime StartTime { get; } + + /// + /// The subject filter to replicate + /// + public string FilterSubject { get; } + + /// + /// External stream reference + /// + public External External { get; } + + /// + /// The subject transforms + /// + public IList SubjectTransforms { get; } + + internal SourceBase(JSONNode sourceBaseNode) + { + Name = sourceBaseNode[ApiConstants.Name].Value; + StartSeq = sourceBaseNode[ApiConstants.OptStartSeq].AsUlong; + StartTime = JsonUtils.AsDate(sourceBaseNode[ApiConstants.OptStartTime]); + FilterSubject = sourceBaseNode[ApiConstants.FilterSubject].Value; + External = External.OptionalInstance(sourceBaseNode[ApiConstants.External]); + SubjectTransforms = SubjectTransform.OptionalListOf(sourceBaseNode[ApiConstants.SubjectTransforms]); + } + + protected SourceBase(string name, ulong startSeq, DateTime startTime, string filterSubject, External external, + IList subjectTransforms = null) + { + Name = name; + StartSeq = startSeq; + StartTime = startTime; + FilterSubject = filterSubject; + External = external; + SubjectTransforms = subjectTransforms; + } + + protected SourceBase(ISourceBaseBuilder isbb) + { + Name = isbb.Name; + StartSeq = isbb.StartSeq; + StartTime = isbb.StartTime; + FilterSubject = isbb.FilterSubject; + External = isbb.External; + SubjectTransforms = isbb.SubjectTransforms; + } + + public override JSONNode ToJsonNode() + { + JSONObject jso = new JSONObject(); + JsonUtils.AddField(jso, ApiConstants.Name, Name); + JsonUtils.AddField(jso, ApiConstants.OptStartSeq, StartSeq); + JsonUtils.AddField(jso, ApiConstants.OptStartTime, JsonUtils.ToString(StartTime)); + JsonUtils.AddField(jso, ApiConstants.FilterSubject, FilterSubject); + JsonUtils.AddField(jso, ApiConstants.External, External); + JsonUtils.AddField(jso, ApiConstants.SubjectTransforms, SubjectTransforms); + return jso; + } + + public interface ISourceBaseBuilder + { + string Name { get; } + ulong StartSeq { get; } + DateTime StartTime { get; } + string FilterSubject { get; } + External External { get; } + IList SubjectTransforms { get; } + } + + public abstract class SourceBaseBuilder : ISourceBaseBuilder + { + protected string _name; + protected ulong _startSeq; + protected DateTime _startTime; + protected string _filterSubject; + protected External _external; + protected IList _subjectTransforms; + + public string Name => _name; + public ulong StartSeq => _startSeq; + public DateTime StartTime => _startTime; + public string FilterSubject => _filterSubject; + public External External => _external; + public IList SubjectTransforms => _subjectTransforms; + + protected abstract TBuilder GetThis(); + + protected SourceBaseBuilder() { } + + protected SourceBaseBuilder(SourceBase sourceBase) + { + _name = sourceBase.Name; + _startSeq = sourceBase.StartSeq; + _startTime = sourceBase.StartTime; + _filterSubject = sourceBase.FilterSubject; + _external = sourceBase.External; + _subjectTransforms = sourceBase.SubjectTransforms; + } + + /// + /// Set the source name. + /// + /// the name + /// The Builder + public TBuilder WithName(string name) + { + _name = name; + return GetThis(); + } + + /// + /// Set the start sequence. + /// + /// the start sequence + /// The Builder + public TBuilder WithStartSeq(ulong startSeq) + { + _startSeq = startSeq; + return GetThis(); + } + + /// + /// Set the start time. + /// + /// the start time + /// The Builder + public TBuilder WithStartTime(DateTime startTime) + { + _startTime = startTime; + return GetThis(); + } + + /// + /// Set the filter subject. + /// + /// the filterSubject + /// The Builder + public TBuilder WithFilterSubject(string filterSubject) + { + _filterSubject = filterSubject; + return GetThis(); + } + + /// + /// Set the external reference. + /// + /// the external + /// The Builder + public TBuilder WithExternal(External external) + { + _external = external; + return GetThis(); + } + + /// + /// Set the external reference by using a domain based prefix. + /// + /// the domain + /// The Builder + public TBuilder WithDomain(string domain) + { + string prefix = JetStreamOptions.ConvertDomainToPrefix(domain); + _external = prefix == null ? null : External.Builder().WithApi(prefix).Build(); + return GetThis(); + } + + /// + /// Set the subject transforms. + /// + /// the subjectTransforms + /// The Builder + public TBuilder WithSubjectTransforms(params SubjectTransform[] subjectTransforms) + { + _subjectTransforms = subjectTransforms; + return GetThis(); + } + + /// + /// Set the subject transforms. + /// + /// the subjectTransforms + /// The Builder + public TBuilder WithSubjectTransforms(IList subjectTransforms) + { + _subjectTransforms = subjectTransforms; + return GetThis(); + } + + /// + /// Build a Source object + /// + /// The Source + public abstract TSourceBase Build(); + } + + protected bool Equals(SourceBase other) + { + return Name == other.Name + && StartSeq == other.StartSeq + && StartTime.Equals(other.StartTime) + && FilterSubject == other.FilterSubject + && Equals(External, other.External) + && Validator.SequenceEqual(SubjectTransforms, other.SubjectTransforms); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((SourceBase)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = (SubjectTransforms != null ? SubjectTransforms.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (Name != null ? Name.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ StartSeq.GetHashCode(); + hashCode = (hashCode * 397) ^ StartTime.GetHashCode(); + hashCode = (hashCode * 397) ^ (FilterSubject != null ? FilterSubject.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (External != null ? External.GetHashCode() : 0); + return hashCode; + } + } + } +} \ No newline at end of file diff --git a/src/NATS.Client/JetStream/SourceInfoBase.cs b/src/NATS.Client/JetStream/SourceInfoBase.cs index ed53d95c4..7e9a4aca9 100644 --- a/src/NATS.Client/JetStream/SourceInfoBase.cs +++ b/src/NATS.Client/JetStream/SourceInfoBase.cs @@ -11,6 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Collections.Generic; using NATS.Client.Internals; using NATS.Client.Internals.SimpleJSON; @@ -21,6 +22,8 @@ public abstract class SourceInfoBase public string Name { get; } public ulong Lag { get; } public Duration Active { get; } + public External External { get; } + public IList SubjectTransforms { get; } public Error Error { get; } internal SourceInfoBase(JSONNode sourceInfoBaseNode) @@ -28,7 +31,9 @@ internal SourceInfoBase(JSONNode sourceInfoBaseNode) Name = sourceInfoBaseNode[ApiConstants.Name].Value; Lag = sourceInfoBaseNode[ApiConstants.Lag].AsUlong; Active = JsonUtils.AsDuration(sourceInfoBaseNode, ApiConstants.Active, Duration.Zero); + External = External.OptionalInstance(sourceInfoBaseNode[ApiConstants.External]); Error = Error.OptionalInstance(sourceInfoBaseNode[ApiConstants.Error]); + SubjectTransforms = SubjectTransform.OptionalListOf(sourceInfoBaseNode[ApiConstants.SubjectTransforms]); } } } diff --git a/src/NATS.Client/JetStream/StreamConfiguration.cs b/src/NATS.Client/JetStream/StreamConfiguration.cs index 506d8e751..81c3ef353 100644 --- a/src/NATS.Client/JetStream/StreamConfiguration.cs +++ b/src/NATS.Client/JetStream/StreamConfiguration.cs @@ -575,9 +575,12 @@ public StreamConfigurationBuilder WithSources(List sources) { /// the stream's sources /// The StreamConfigurationBuilder public StreamConfigurationBuilder AddSources(params Source[] sources) { - if (sources != null) { - foreach (Source source in sources) { - if (source != null && !_sources.Contains(source)) { + if (sources != null) + { + foreach (Source source in sources) + { + if (source != null && !_sources.Contains(source)) + { _sources.Add(source); } } @@ -591,9 +594,12 @@ public StreamConfigurationBuilder AddSources(params Source[] sources) { /// the stream's sources /// The StreamConfigurationBuilder public StreamConfigurationBuilder AddSources(List sources) { - if (sources != null) { - foreach (Source source in sources) { - if (source != null && !_sources.Contains(source)) { + if (sources != null) + { + foreach (Source source in sources) + { + if (source != null && !_sources.Contains(source)) + { _sources.Add(source); } } diff --git a/src/NATS.Client/JetStream/SubjectTransform.cs b/src/NATS.Client/JetStream/SubjectTransform.cs index a56858d1e..d46fb28cf 100644 --- a/src/NATS.Client/JetStream/SubjectTransform.cs +++ b/src/NATS.Client/JetStream/SubjectTransform.cs @@ -11,6 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Collections.Generic; using NATS.Client.Internals.SimpleJSON; using static NATS.Client.Internals.JsonUtils; @@ -35,6 +36,21 @@ internal static SubjectTransform OptionalInstance(JSONNode subjectTransformNode) { return subjectTransformNode.Count == 0 ? null : new SubjectTransform(subjectTransformNode); } + + internal static IList OptionalListOf(JSONNode subjectTransformListNode) + { + if (subjectTransformListNode == null) + { + return null; + } + + IList list = new List(); + foreach (var subjectTransformNode in subjectTransformListNode.Children) + { + list.Add(new SubjectTransform(subjectTransformNode)); + } + return list.Count == 0 ? null : list; + } private SubjectTransform(JSONNode subjectTransformNode) { @@ -80,7 +96,7 @@ public sealed class SubjectTransformBuilder { /// Set the Published subject matching filter. /// /// the source - /// + /// The SubjectTransformBuilder public SubjectTransformBuilder WithSource(string source) { _source = source; return this; @@ -90,7 +106,7 @@ public SubjectTransformBuilder WithSource(string source) { /// Set the SubjectTransform Subject template /// /// the destination - /// + /// The SubjectTransformBuilder public SubjectTransformBuilder WithDestination(string destination) { _destination = destination; return this; @@ -99,10 +115,28 @@ public SubjectTransformBuilder WithDestination(string destination) { /// /// Build a SubjectTransform object /// - /// The SubjectTransform + /// The SubjectTransform object public SubjectTransform Build() { return new SubjectTransform(_source, _destination); } } + + private bool Equals(SubjectTransform other) + { + return Source == other.Source && Destination == other.Destination; + } + + public override bool Equals(object obj) + { + return ReferenceEquals(this, obj) || obj is SubjectTransform other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return ((Source != null ? Source.GetHashCode() : 0) * 397) ^ (Destination != null ? Destination.GetHashCode() : 0); + } + } } } diff --git a/src/Tests/UnitTests/Data/StreamConfiguration.json b/src/Tests/UnitTests/Data/StreamConfiguration.json index 6e1806443..450670ee5 100644 --- a/src/Tests/UnitTests/Data/StreamConfiguration.json +++ b/src/Tests/UnitTests/Data/StreamConfiguration.json @@ -35,8 +35,8 @@ "headers_only": true }, "subject_transform": { - "src": "st.>", - "dest": "stdest.>" + "src": "sc_st_src0", + "dest": "sc_st_dest0" }, "consumer_limits": { "inactive_threshold": 50000000000, @@ -50,7 +50,11 @@ "external": { "api": "apithing", "deliver": "dlvrsub" - } + }, + "subject_transforms": [ + {"src":"m_st_src0","dest":"m_st_dest0"}, + {"src":"m_st_src1","dest":"m_st_dest1"} + ] }, "sources": [ { @@ -61,7 +65,11 @@ "external": { "api": "s0api", "deliver": "s0dlvrsub" - } + }, + "subject_transforms": [ + {"src":"s0_st_src0","dest":"s0_st_dest0"}, + {"src":"s0_st_src1","dest":"s0_st_dest1"} + ] }, { "name": "s1", @@ -71,7 +79,11 @@ "external": { "api": "s1api", "deliver": "s1dlvrsub" - } + }, + "subject_transforms": [ + {"src":"s1_st_src0","dest":"s1_st_dest0"}, + {"src":"s1_st_src1","dest":"s1_st_dest1"} + ] } ] } \ No newline at end of file diff --git a/src/Tests/UnitTests/Data/StreamInfo.json b/src/Tests/UnitTests/Data/StreamInfo.json index d7bbf19e0..28f9e7981 100644 --- a/src/Tests/UnitTests/Data/StreamInfo.json +++ b/src/Tests/UnitTests/Data/StreamInfo.json @@ -74,18 +74,42 @@ "mirror": { "name": "mname", "lag": 16, - "active": 160000000000 + "active": 160000000000, + "external": { + "api": "api16", + "deliver": "dlvr16" + }, + "subject_transforms": [ + {"src":"16_st_src0","dest":"16_st_dest0"}, + {"src":"16_st_src1","dest":"16_st_dest1"} + ] }, "sources": [ { "name": "sname17", "lag": 17, - "active": 170000000000 + "active": 170000000000, + "external": { + "api": "api17", + "deliver": "dlvr17" + }, + "subject_transforms": [ + {"src":"17_st_src0","dest":"17_st_dest0"}, + {"src":"17_st_src1","dest":"17_st_dest1"} + ] }, { "name": "sname18", "lag": 18, - "active": 180000000000 + "active": 180000000000, + "external": { + "api": "api18", + "deliver": "dlvr18" + }, + "subject_transforms": [ + {"src":"18_st_src0","dest":"18_st_dest0"}, + {"src":"18_st_src1","dest":"18_st_dest1"} + ] } ] } diff --git a/src/Tests/UnitTests/JetStream/TestStreamConfiguration.cs b/src/Tests/UnitTests/JetStream/TestStreamConfiguration.cs index 3ff6d0b0b..f9aceab63 100644 --- a/src/Tests/UnitTests/JetStream/TestStreamConfiguration.cs +++ b/src/Tests/UnitTests/JetStream/TestStreamConfiguration.cs @@ -314,16 +314,22 @@ private void Validate(StreamConfiguration sc, bool serverTest) Assert.Equal(5, sc.Replicas); Assert.Equal("twnr", sc.TemplateOwner); + Assert.NotNull(sc.Mirror); Assert.Equal("eman", sc.Mirror.Name); Assert.Equal(736U, sc.Mirror.StartSeq); Assert.Equal(zdt, sc.Mirror.StartTime); Assert.Equal("mfsub", sc.Mirror.FilterSubject); + Assert.NotNull(sc.Mirror.External); + Assert.Equal("apithing", sc.Mirror.External.Api); + Assert.Equal("dlvrsub", sc.Mirror.External.Deliver); + + ValidateSubjectTransforms(sc.Mirror.SubjectTransforms, 2, "m"); + Assert.Equal(2, sc.Sources.Count); - Assert.Collection(sc.Sources, - item => ValidateSource(item, "s0", 737, "s0sub", "s0api", "s0dlvrsub", zdt), - item => ValidateSource(item, "s1", 738, "s1sub", "s1api", "s1dlvrsub", zdt)); + ValidateSource(sc.Sources[0], 0, zdt); + ValidateSource(sc.Sources[1], 1, zdt); Assert.Single(sc.Metadata); Assert.Equal("meta-bar", sc.Metadata["meta-foo"]); @@ -331,9 +337,9 @@ private void Validate(StreamConfiguration sc, bool serverTest) Assert.Equal(CompressionOption.S2, sc.CompressionOption); - Assert.NotNull(sc.SubjectTransform); - Assert.Equal("st.>", sc.SubjectTransform.Source); - Assert.Equal("stdest.>", sc.SubjectTransform.Destination); + IList stList = new List(); + stList.Add(sc.SubjectTransform); + ValidateSubjectTransforms(stList, 1, "sc"); Assert.NotNull(sc.ConsumerLimits); Assert.Equal(Duration.OfSeconds(50), sc.ConsumerLimits.InactiveThreshold); @@ -342,16 +348,30 @@ private void Validate(StreamConfiguration sc, bool serverTest) } } - private void ValidateSource(Source source, string name, ulong seq, string filter, string api, string deliver, DateTime zdt) + internal static void ValidateSubjectTransforms(IList subjectTransforms, int count, String name) { + Assert.NotNull(subjectTransforms); + Assert.Equal(count, subjectTransforms.Count); + for (int x = 0; x < count; x++) { + SubjectTransform st = subjectTransforms[x]; + Assert.Equal(name + "_st_src" + x, st.Source); + Assert.Equal(name + "_st_dest" + x, st.Destination); + } + } + + internal static void ValidateSource(Source source, ulong index, DateTime zdt) { + ulong seq = 737 + index; + string name = "s" + index; Assert.Equal(name, source.Name); Assert.Equal(seq, source.StartSeq); Assert.Equal(zdt, source.StartTime); - Assert.Equal(filter, source.FilterSubject); + Assert.Equal($"{name}sub", source.FilterSubject); Assert.NotNull(source.External); - Assert.Equal(api, source.External.Api); - Assert.Equal(deliver, source.External.Deliver); + Assert.Equal($"{name}api", source.External.Api); + Assert.Equal($"{name}dlvrsub", source.External.Deliver); + + ValidateSubjectTransforms(source.SubjectTransforms, 2, name); } [Fact] diff --git a/src/Tests/UnitTests/JetStream/TestStreamInfo.cs b/src/Tests/UnitTests/JetStream/TestStreamInfo.cs index 0d253454b..06db61354 100644 --- a/src/Tests/UnitTests/JetStream/TestStreamInfo.cs +++ b/src/Tests/UnitTests/JetStream/TestStreamInfo.cs @@ -12,6 +12,7 @@ // limitations under the License. using System.Collections.Generic; +using NATS.Client.Internals; using NATS.Client.JetStream; using Xunit; @@ -110,15 +111,28 @@ public void JsonIsReadProperly() Assert.Equal("mname", si.MirrorInfo.Name); Assert.Equal(16u, si.MirrorInfo.Lag); Assert.Equal(160000000000, si.MirrorInfo.Active.Nanos); + Assert.Null(si.MirrorInfo.Error); + ValidateExternal(si.MirrorInfo.External, 16); // List SourceInfos Assert.Equal(2, si.SourceInfos.Count); - Assert.Equal("sname17", si.SourceInfos[0].Name); - Assert.Equal(17u, si.SourceInfos[0].Lag); - Assert.Equal(170000000000, si.SourceInfos[0].Active.Nanos); - Assert.Equal("sname18", si.SourceInfos[1].Name); - Assert.Equal(18u, si.SourceInfos[1].Lag); - Assert.Equal(180000000000, si.SourceInfos[1].Active.Nanos); + ValidateSourceInfo(si.SourceInfos[0], 17); + ValidateSourceInfo(si.SourceInfos[1], 18); } + + private static void ValidateSourceInfo(SourceInfo sourceInfo, ulong id) { + Assert.Equal("sname" + id, sourceInfo.Name); + Assert.Equal(id, sourceInfo.Lag); + Assert.Equal(Duration.OfNanos((long)id * 10000000000L), sourceInfo.Active); + ValidateExternal(sourceInfo.External, id); + TestStreamConfiguration.ValidateSubjectTransforms(sourceInfo.SubjectTransforms, 2, "" + id); + } + + private static void ValidateExternal(External e, ulong id) { + Assert.NotNull(e); + Assert.Equal("api" + id, e.Api); + Assert.Equal("dlvr" + id, e.Deliver); + } + } }