diff --git a/Docs/Publish.png b/Docs/Publish.png new file mode 100644 index 0000000..b1e3341 Binary files /dev/null and b/Docs/Publish.png differ diff --git a/Docs/Subscribe.png b/Docs/Subscribe.png new file mode 100644 index 0000000..06b429f Binary files /dev/null and b/Docs/Subscribe.png differ diff --git a/Docs/TapetiPublish.png b/Docs/TapetiPublish.png new file mode 100644 index 0000000..40b888c Binary files /dev/null and b/Docs/TapetiPublish.png differ diff --git a/PettingZoo.Core/Connection/ISubscriber.cs b/PettingZoo.Core/Connection/ISubscriber.cs index 9beb336..e18c9e1 100644 --- a/PettingZoo.Core/Connection/ISubscriber.cs +++ b/PettingZoo.Core/Connection/ISubscriber.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace PettingZoo.Core.Connection { @@ -10,6 +11,7 @@ namespace PettingZoo.Core.Connection event EventHandler? MessageReceived; + IEnumerable GetInitialMessages(); void Start(); } diff --git a/PettingZoo.Core/ExportImport/BaseProgressDecorator.cs b/PettingZoo.Core/ExportImport/BaseProgressDecorator.cs new file mode 100644 index 0000000..8e02cc4 --- /dev/null +++ b/PettingZoo.Core/ExportImport/BaseProgressDecorator.cs @@ -0,0 +1,33 @@ +using System; + +namespace PettingZoo.Core.ExportImport +{ + public abstract class BaseProgressDecorator + { + private static readonly TimeSpan DefaultReportInterval = TimeSpan.FromMilliseconds(100); + + private readonly IProgress progress; + private readonly long reportInterval; + private long lastReport; + + protected BaseProgressDecorator(IProgress progress, TimeSpan? reportInterval = null) + { + this.progress = progress; + this.reportInterval = (int)(reportInterval ?? DefaultReportInterval).TotalMilliseconds; + } + + + protected abstract int GetProgress(); + + protected void UpdateProgress() + { + // Because this method is called pretty frequently, not having DateTime's small overhead is worth it + var now = Environment.TickCount64; + if (now - lastReport < reportInterval) + return; + + progress.Report(GetProgress()); + lastReport = now; + } + } +} diff --git a/PettingZoo.Core/ExportImport/ExportImportFormatProvider.cs b/PettingZoo.Core/ExportImport/ExportImportFormatProvider.cs new file mode 100644 index 0000000..1482bf2 --- /dev/null +++ b/PettingZoo.Core/ExportImport/ExportImportFormatProvider.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; + +namespace PettingZoo.Core.ExportImport +{ + public class ExportImportFormatProvider : IExportImportFormatProvider + { + private readonly List exportFormats; + private readonly List importFormats; + + + public IEnumerable ExportFormats => exportFormats; + public IEnumerable ImportFormats => importFormats; + + + public ExportImportFormatProvider(params IExportImportFormat[] formats) + { + exportFormats = new List(formats.Where(f => f is IExportFormat).Cast()); + importFormats = new List(formats.Where(f => f is IImportFormat).Cast()); + } + } +} diff --git a/PettingZoo.Core/ExportImport/IExportImportFormat.cs b/PettingZoo.Core/ExportImport/IExportImportFormat.cs new file mode 100644 index 0000000..e586348 --- /dev/null +++ b/PettingZoo.Core/ExportImport/IExportImportFormat.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using PettingZoo.Core.Connection; + +namespace PettingZoo.Core.ExportImport +{ + public interface IExportImportFormat + { + string Filter { get; } + } + + + public interface IExportFormat : IExportImportFormat + { + Task Export(Stream stream, IEnumerable messages, CancellationToken cancellationToken); + } + + + public interface IImportFormat : IExportImportFormat + { + Task> Import(Stream stream, CancellationToken cancellationToken); + } +} diff --git a/PettingZoo.Core/ExportImport/IExportImportFormatProvider.cs b/PettingZoo.Core/ExportImport/IExportImportFormatProvider.cs new file mode 100644 index 0000000..1991668 --- /dev/null +++ b/PettingZoo.Core/ExportImport/IExportImportFormatProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace PettingZoo.Core.ExportImport +{ + public interface IExportImportFormatProvider + { + public IEnumerable ExportFormats { get; } + public IEnumerable ImportFormats { get; } + } +} diff --git a/PettingZoo.Core/ExportImport/ImportSubscriber.cs b/PettingZoo.Core/ExportImport/ImportSubscriber.cs new file mode 100644 index 0000000..067fee4 --- /dev/null +++ b/PettingZoo.Core/ExportImport/ImportSubscriber.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using PettingZoo.Core.Connection; + +namespace PettingZoo.Core.ExportImport +{ + public class ImportSubscriber : ISubscriber + { + private readonly IReadOnlyList messages; + + public string? QueueName { get; } + public string? Exchange => null; + public string? RoutingKey => null; + public event EventHandler? MessageReceived; + + + public ImportSubscriber(string filename, IReadOnlyList messages) + { + QueueName = Path.GetFileName(filename); + this.messages = messages; + } + + + public ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + return default; + } + + + public IEnumerable GetInitialMessages() + { + return messages; + } + + + public void Start() + { + } + } +} diff --git a/PettingZoo.Core/ExportImport/ListEnumerableProgressDecorator.cs b/PettingZoo.Core/ExportImport/ListEnumerableProgressDecorator.cs new file mode 100644 index 0000000..d74425a --- /dev/null +++ b/PettingZoo.Core/ExportImport/ListEnumerableProgressDecorator.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace PettingZoo.Core.ExportImport +{ + public class ListEnumerableProgressDecorator : BaseProgressDecorator, IEnumerable + { + private readonly IReadOnlyList decoratedList; + private int position; + + + /// + /// Wraps an IReadOnlyList and provides reports to the IProgress when it is enumerated. + /// + /// The IReadOnlyList to decorate. + /// Receives progress reports. The value will be a percentage (0 - 100). + /// The minimum time between reports. + public ListEnumerableProgressDecorator(IReadOnlyList decoratedList, IProgress progress, TimeSpan? reportInterval = null) + : base(progress, reportInterval) + { + this.decoratedList = decoratedList; + } + + + protected override int GetProgress() + { + return decoratedList.Count > 0 + ? (int)Math.Truncate((double)position / decoratedList.Count * 100) + : 0; + } + + + protected void AfterNext() + { + position++; + UpdateProgress(); + } + + + protected void Reset() + { + position = 0; + UpdateProgress(); + } + + + public IEnumerator GetEnumerator() + { + return new EnumerableWrapper(this, decoratedList.GetEnumerator()); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + + private class EnumerableWrapper : IEnumerator + { + private readonly ListEnumerableProgressDecorator owner; + private readonly IEnumerator decoratedEnumerator; + + + public EnumerableWrapper(ListEnumerableProgressDecorator owner, IEnumerator decoratedEnumerator) + { + this.owner = owner; + this.decoratedEnumerator = decoratedEnumerator; + } + + + public bool MoveNext() + { + var result = decoratedEnumerator.MoveNext(); + if (result) + owner.AfterNext(); + + return result; + } + + + public void Reset() + { + decoratedEnumerator.Reset(); + owner.Reset(); + } + + + public T Current => decoratedEnumerator.Current; + object IEnumerator.Current => decoratedEnumerator.Current!; + + + public void Dispose() + { + GC.SuppressFinalize(this); + decoratedEnumerator.Dispose(); + } + } + } +} diff --git a/PettingZoo.Core/ExportImport/StreamProgressDecorator.cs b/PettingZoo.Core/ExportImport/StreamProgressDecorator.cs new file mode 100644 index 0000000..76ee710 --- /dev/null +++ b/PettingZoo.Core/ExportImport/StreamProgressDecorator.cs @@ -0,0 +1,157 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace PettingZoo.Core.ExportImport +{ + public class StreamProgressDecorator : BaseProgressDecorator + { + private readonly StreamWrapper streamWrapper; + + public Stream Stream => streamWrapper; + + + /// + /// Wraps a Stream and provides reports to the IProgress. + /// + /// + /// Use the Stream property to pass along to the method you want to monitor the progress on. + /// If the consumer seeks around in the stream a lot the progress will not be linear, but that + /// seems to be a trend anyways with progress bars, so enjoy your modern experience! + /// + /// The stream to decorate. + /// Receives progress reports. The value will be a percentage (0 - 100). + /// The minimum time between reports. + public StreamProgressDecorator(Stream decoratedStream, IProgress progress, TimeSpan? reportInterval = null) + : base(progress, reportInterval) + { + streamWrapper = new StreamWrapper(this, decoratedStream); + } + + + protected override int GetProgress() + { + return streamWrapper.DecoratedStream.Length > 0 + ? (int)Math.Truncate((double)streamWrapper.DecoratedStream.Position / streamWrapper.DecoratedStream.Length * 100) + : 0; + } + + + private class StreamWrapper : Stream + { + private readonly StreamProgressDecorator owner; + public readonly Stream DecoratedStream; + + public override bool CanRead => DecoratedStream.CanRead; + public override bool CanSeek => DecoratedStream.CanSeek; + public override bool CanWrite => DecoratedStream.CanWrite; + public override long Length => DecoratedStream.Length; + + public override long Position + { + get => DecoratedStream.Position; + set => DecoratedStream.Position = value; + } + + + public StreamWrapper(StreamProgressDecorator owner, Stream decoratedStream) + { + this.owner = owner; + this.DecoratedStream = decoratedStream; + } + + + public override void Flush() + { + DecoratedStream.Flush(); + } + + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return DecoratedStream.FlushAsync(cancellationToken); + } + + + public override int Read(byte[] buffer, int offset, int count) + { + var result = DecoratedStream.Read(buffer, offset, count); + owner.UpdateProgress(); + return result; + } + + public override int Read(Span buffer) + { + var result = DecoratedStream.Read(buffer); + owner.UpdateProgress(); + return result; + } + + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + #pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' + var result = await DecoratedStream.ReadAsync(buffer, offset, count, cancellationToken); + #pragma warning restore CA1835 + owner.UpdateProgress(); + return result; + } + + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = new()) + { + var result = DecoratedStream.ReadAsync(buffer, cancellationToken); + owner.UpdateProgress(); + return result; + } + + + public override long Seek(long offset, SeekOrigin origin) + { + var result = DecoratedStream.Seek(offset, origin); + owner.UpdateProgress(); + return result; + } + + + public override void SetLength(long value) + { + DecoratedStream.SetLength(value); + owner.UpdateProgress(); + } + + + public override void Write(byte[] buffer, int offset, int count) + { + DecoratedStream.Write(buffer, offset, count); + owner.UpdateProgress(); + } + + + public override void Write(ReadOnlySpan buffer) + { + DecoratedStream.Write(buffer); + owner.UpdateProgress(); + } + + + public override async Task WriteAsync(byte[] buffer, int offset, int count, + CancellationToken cancellationToken) + { + #pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' + await DecoratedStream.WriteAsync(buffer, offset, count, cancellationToken); + #pragma warning restore CA1835 + owner.UpdateProgress(); + } + + + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, + CancellationToken cancellationToken = new()) + { + await DecoratedStream.WriteAsync(buffer, cancellationToken); + owner.UpdateProgress(); + } + } + } +} diff --git a/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs b/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs index 2c24db9..9da4068 100644 --- a/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs +++ b/PettingZoo.RabbitMQ/RabbitMQClientSubscriber.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using PettingZoo.Core.Connection; using RabbitMQ.Client; @@ -38,6 +40,12 @@ namespace PettingZoo.RabbitMQ } + public IEnumerable GetInitialMessages() + { + return Enumerable.Empty(); + } + + public void Start() { started = true; diff --git a/PettingZoo.Tapeti/Export/BaseTapetiCmdExportImportFormat.cs b/PettingZoo.Tapeti/Export/BaseTapetiCmdExportImportFormat.cs new file mode 100644 index 0000000..6bfd721 --- /dev/null +++ b/PettingZoo.Tapeti/Export/BaseTapetiCmdExportImportFormat.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using PettingZoo.Core.ExportImport; + +namespace PettingZoo.Tapeti.Export +{ + public abstract class BaseTapetiCmdExportImportFormat : IExportImportFormat + { + public string Filter => TapetiCmdImportExportStrings.TapetiCmdFilter; + } + + + // It would be nicer if Tapeti.Cmd exposed it's file format in a NuGet package... if only I knew the author ¯\_(ツ)_/¯ + public class SerializableMessage + { + //public ulong DeliveryTag; + //public bool Redelivered; + public string? Exchange; + public string? RoutingKey; + //public string? Queue; + + // ReSharper disable once FieldCanBeMadeReadOnly.Local - must be settable by JSON deserialization + public SerializableMessageProperties? Properties; + + public JObject? Body; + public byte[]? RawBody; + } + + + public class SerializableMessageProperties + { + public string? AppId; + //public string? ClusterId; + public string? ContentEncoding; + public string? ContentType; + public string? CorrelationId; + public byte? DeliveryMode; + public string? Expiration; + public IDictionary? Headers; + public string? MessageId; + public byte? Priority; + public string? ReplyTo; + public long? Timestamp; + public string? Type; + public string? UserId; + } +} diff --git a/PettingZoo.Tapeti/Export/TapetiCmdExportFormat.cs b/PettingZoo.Tapeti/Export/TapetiCmdExportFormat.cs new file mode 100644 index 0000000..7967827 --- /dev/null +++ b/PettingZoo.Tapeti/Export/TapetiCmdExportFormat.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PettingZoo.Core.Connection; +using PettingZoo.Core.ExportImport; + + +namespace PettingZoo.Tapeti.Export +{ + public class TapetiCmdExportFormat : BaseTapetiCmdExportImportFormat, IExportFormat + { + private static readonly JsonSerializerSettings SerializerSettings = new() + { + NullValueHandling = NullValueHandling.Ignore + }; + + + public async Task Export(Stream stream, IEnumerable messages, CancellationToken cancellationToken) + { + await using var exportFile = new StreamWriter(stream, Encoding.UTF8); + + foreach (var message in messages) + { + if (cancellationToken.IsCancellationRequested) + break; + + var serializableMessage = new SerializableMessage + { + Exchange = message.Exchange, + RoutingKey = message.RoutingKey, + Properties = new SerializableMessageProperties + { + AppId = message.Properties.AppId, + ContentEncoding = message.Properties.ContentEncoding, + ContentType = message.Properties.ContentType, + CorrelationId = message.Properties.CorrelationId, + DeliveryMode = message.Properties.DeliveryMode switch + { + MessageDeliveryMode.Persistent => 2, + _ => 1 + }, + Expiration = message.Properties.Expiration, + Headers = message.Properties.Headers.Count > 0 ? message.Properties.Headers.ToDictionary(p => p.Key, p => p.Value) : null, + MessageId = message.Properties.MessageId, + Priority = message.Properties.Priority, + ReplyTo = message.Properties.ReplyTo, + Timestamp = message.Properties.Timestamp.HasValue ? new DateTimeOffset(message.Properties.Timestamp.Value).ToUnixTimeSeconds() : null, + Type = message.Properties.Type, + UserId = message.Properties.UserId + } + }; + + + var useRawBody = true; + if (message.Properties.ContentType == @"application/json") + { + try + { + if (JToken.Parse(Encoding.UTF8.GetString(message.Body)) is JObject jsonBody) + { + serializableMessage.Body = jsonBody; + useRawBody = false; + } + } + catch + { + // Use raw body + } + } + + if (useRawBody) + serializableMessage.RawBody = message.Body; + + var serialized = JsonConvert.SerializeObject(serializableMessage, SerializerSettings); + await exportFile.WriteLineAsync(serialized); + } + } + } +} diff --git a/PettingZoo.Tapeti/Export/TapetiCmdImportExportStrings.Designer.cs b/PettingZoo.Tapeti/Export/TapetiCmdImportExportStrings.Designer.cs new file mode 100644 index 0000000..a9d3bf5 --- /dev/null +++ b/PettingZoo.Tapeti/Export/TapetiCmdImportExportStrings.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace PettingZoo.Tapeti.Export { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class TapetiCmdImportExportStrings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal TapetiCmdImportExportStrings() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.Tapeti.Export.TapetiCmdImportExportStrings", typeof(TapetiCmdImportExportStrings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Tapeti.Cmd single-file JSON (*.json)|*.json. + /// + internal static string TapetiCmdFilter { + get { + return ResourceManager.GetString("TapetiCmdFilter", resourceCulture); + } + } + } +} diff --git a/PettingZoo.Tapeti/Export/TapetiCmdImportExportStrings.resx b/PettingZoo.Tapeti/Export/TapetiCmdImportExportStrings.resx new file mode 100644 index 0000000..4978050 --- /dev/null +++ b/PettingZoo.Tapeti/Export/TapetiCmdImportExportStrings.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Tapeti.Cmd single-file JSON (*.json)|*.json + + \ No newline at end of file diff --git a/PettingZoo.Tapeti/Export/TapetiCmdImportFormat.cs b/PettingZoo.Tapeti/Export/TapetiCmdImportFormat.cs new file mode 100644 index 0000000..58f88d4 --- /dev/null +++ b/PettingZoo.Tapeti/Export/TapetiCmdImportFormat.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using PettingZoo.Core.Connection; +using PettingZoo.Core.ExportImport; + +namespace PettingZoo.Tapeti.Export +{ + public class TapetiCmdImportFormat : BaseTapetiCmdExportImportFormat, IImportFormat + { + private const int DefaultBufferSize = 1024; + + + public async Task> Import(Stream stream, CancellationToken cancellationToken) + { + var now = DateTime.Now; + using var reader = new StreamReader(stream, Encoding.UTF8, true, DefaultBufferSize, true); + + var messages = new List(); + + while (!reader.EndOfStream && !cancellationToken.IsCancellationRequested) + { + var serialized = await reader.ReadLineAsync(); + if (string.IsNullOrEmpty(serialized)) + continue; + + var serializableMessage = JsonConvert.DeserializeObject(serialized); + if (serializableMessage == null) + continue; + + var body = serializableMessage.Body != null + ? Encoding.UTF8.GetBytes(serializableMessage.Body.ToString(Formatting.Indented)) + : serializableMessage.RawBody; + + var messageTimestamp = serializableMessage.Properties?.Timestamp != null + ? DateTimeOffset.FromUnixTimeSeconds(serializableMessage.Properties.Timestamp.Value).LocalDateTime + : now; + + messages.Add(new ReceivedMessageInfo( + serializableMessage.Exchange ?? "", + serializableMessage.RoutingKey ?? "", + body ?? Array.Empty(), + + // IReadOnlyDictionary is not compatible with IDictionary? wow. + new MessageProperties(serializableMessage.Properties?.Headers?.ToDictionary(p => p.Key, p => p.Value)) + { + AppId = serializableMessage.Properties?.AppId, + ContentEncoding = serializableMessage.Properties?.ContentEncoding, + ContentType = serializableMessage.Properties?.ContentType, + CorrelationId = serializableMessage.Properties?.CorrelationId, + DeliveryMode = serializableMessage.Properties?.DeliveryMode switch + { + 2 => MessageDeliveryMode.Persistent, + _ => MessageDeliveryMode.NonPersistent + }, + Expiration = serializableMessage.Properties?.Expiration, + MessageId = serializableMessage.Properties?.MessageId, + Priority = serializableMessage.Properties?.Priority, + ReplyTo = serializableMessage.Properties?.ReplyTo, + Timestamp = messageTimestamp, + Type = serializableMessage.Properties?.Type, + UserId = serializableMessage.Properties?.UserId + }, + messageTimestamp)); + } + + return messages; + } + } +} diff --git a/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj b/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj index 46db93e..3bf7adf 100644 --- a/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj +++ b/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj @@ -32,19 +32,21 @@ True AssemblyParserStrings.resx + + True + True + TapetiCmdImportExportStrings.resx + + + True + True + TapetiClassLibraryExampleGeneratorStrings.resx + True True ClassSelectionStrings.resx - - True - True - PackageProgressStrings.resx - - - Code - True True @@ -57,25 +59,22 @@ ResXFileCodeGenerator AssemblyParserStrings.Designer.cs + + ResXFileCodeGenerator + TapetiCmdImportExportStrings.Designer.cs + + + ResXFileCodeGenerator + TapetiClassLibraryExampleGeneratorStrings.Designer.cs + PublicResXFileCodeGenerator ClassSelectionStrings.Designer.cs - - PublicResXFileCodeGenerator - PackageProgressStrings.Designer.cs - PublicResXFileCodeGenerator PackageSelectionStrings.Designer.cs - - - $(DefaultXamlRuntime) - Designer - - - diff --git a/PettingZoo.Tapeti/PettingZoo.Tapeti_ooj5vuwa_wpftmp.csproj b/PettingZoo.Tapeti/PettingZoo.Tapeti_ooj5vuwa_wpftmp.csproj new file mode 100644 index 0000000..9e25216 --- /dev/null +++ b/PettingZoo.Tapeti/PettingZoo.Tapeti_ooj5vuwa_wpftmp.csproj @@ -0,0 +1,325 @@ + + + PettingZoo.Tapeti + obj\Debug\ + obj\ + P:\Development\PettingZoo\PettingZoo.Tapeti\obj\ + <_TargetAssemblyProjectName>PettingZoo.Tapeti + + + + net6.0-windows + 0.1 + true + enable + + + + + + + + + + + + + + + + + + + + + True + True + AssemblyParserStrings.resx + + + True + True + TapetiCmdImportExportStrings.resx + + + True + True + TapetiClassLibraryExampleGeneratorStrings.resx + + + True + True + ClassSelectionStrings.resx + + + True + True + PackageSelectionStrings.resx + + + + + ResXFileCodeGenerator + AssemblyParserStrings.Designer.cs + + + ResXFileCodeGenerator + TapetiCmdImportExportStrings.Designer.cs + + + ResXFileCodeGenerator + TapetiClassLibraryExampleGeneratorStrings.Designer.cs + + + PublicResXFileCodeGenerator + ClassSelectionStrings.Designer.cs + + + PublicResXFileCodeGenerator + PackageSelectionStrings.Designer.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/PettingZoo.Tapeti/TapetiClassLibraryExampleGenerator.cs b/PettingZoo.Tapeti/TapetiClassLibraryExampleGenerator.cs index ef2b4e7..6cd0eae 100644 --- a/PettingZoo.Tapeti/TapetiClassLibraryExampleGenerator.cs +++ b/PettingZoo.Tapeti/TapetiClassLibraryExampleGenerator.cs @@ -11,8 +11,8 @@ using PettingZoo.Core.Settings; using PettingZoo.Tapeti.AssemblyLoader; using PettingZoo.Tapeti.NuGet; using PettingZoo.Tapeti.UI.ClassSelection; -using PettingZoo.Tapeti.UI.PackageProgress; using PettingZoo.Tapeti.UI.PackageSelection; +using PettingZoo.WPF.ProgressWindow; using Serilog; namespace PettingZoo.Tapeti @@ -34,8 +34,6 @@ namespace PettingZoo.Tapeti .WithSourcesFrom(Path.Combine(PettingZooPaths.InstallationRoot, @"nuget.config")) .WithSourcesFrom(Path.Combine(PettingZooPaths.AppDataRoot, @"nuget.config")); - var dispatcher = Dispatcher.CurrentDispatcher; - var viewModel = new PackageSelectionViewModel(packageManager); var selectionWindow = new PackageSelectionWindow(viewModel) { @@ -44,27 +42,28 @@ namespace PettingZoo.Tapeti viewModel.Select += (_, args) => { - dispatcher.Invoke(() => + Application.Current.Dispatcher.Invoke(() => { var windowBounds = selectionWindow.RestoreBounds; selectionWindow.Close(); - var progressWindow = new PackageProgressWindow(); + var progressWindow = new ProgressWindow(TapetiClassLibraryExampleGeneratorStrings.ProgressWindowTitle); progressWindow.Left = windowBounds.Left + (windowBounds.Width - progressWindow.Width) / 2; progressWindow.Top = windowBounds.Top + (windowBounds.Height - progressWindow.Height) / 2; progressWindow.Show(); + var cancellationToken = progressWindow.CancellationToken; + Task.Run(async () => { try { - // TODO allow cancelling (by closing the progress window and optionally a Cancel button) - var assemblies = await args.Assemblies.GetAssemblies(progressWindow, CancellationToken.None); + var assemblies = await args.Assemblies.GetAssemblies(progressWindow, cancellationToken); // var classes = var examples = LoadExamples(assemblies); - dispatcher.Invoke(() => + Application.Current.Dispatcher.Invoke(() => { progressWindow.Close(); progressWindow = null; @@ -89,15 +88,16 @@ namespace PettingZoo.Tapeti } catch (Exception e) { - dispatcher.Invoke(() => + Application.Current.Dispatcher.Invoke(() => { // ReSharper disable once ConstantConditionalAccessQualifier - if I remove it, there's a "Dereference of a possibly null reference" warning instead progressWindow?.Close(); - MessageBox.Show($"Error while loading assembly: {e.Message}", "Petting Zoo - Exception", MessageBoxButton.OK, MessageBoxImage.Error); + if (e is not OperationCanceledException) + MessageBox.Show($"Error while loading assembly: {e.Message}", "Petting Zoo - Exception", MessageBoxButton.OK, MessageBoxImage.Error); }); } - }); + }, CancellationToken.None); }); }; diff --git a/PettingZoo.Tapeti/TapetiClassLibraryExampleGeneratorStrings.Designer.cs b/PettingZoo.Tapeti/TapetiClassLibraryExampleGeneratorStrings.Designer.cs new file mode 100644 index 0000000..935784c --- /dev/null +++ b/PettingZoo.Tapeti/TapetiClassLibraryExampleGeneratorStrings.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace PettingZoo.Tapeti { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class TapetiClassLibraryExampleGeneratorStrings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal TapetiClassLibraryExampleGeneratorStrings() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.Tapeti.TapetiClassLibraryExampleGeneratorStrings", typeof(TapetiClassLibraryExampleGeneratorStrings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Reading message classes.... + /// + internal static string ProgressWindowTitle { + get { + return ResourceManager.GetString("ProgressWindowTitle", resourceCulture); + } + } + } +} diff --git a/PettingZoo.Tapeti/UI/PackageProgress/PackageProgressStrings.resx b/PettingZoo.Tapeti/TapetiClassLibraryExampleGeneratorStrings.resx similarity index 96% rename from PettingZoo.Tapeti/UI/PackageProgress/PackageProgressStrings.resx rename to PettingZoo.Tapeti/TapetiClassLibraryExampleGeneratorStrings.resx index 1d6d493..6bee679 100644 --- a/PettingZoo.Tapeti/UI/PackageProgress/PackageProgressStrings.resx +++ b/PettingZoo.Tapeti/TapetiClassLibraryExampleGeneratorStrings.resx @@ -112,12 +112,12 @@ 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + Reading message classes... \ No newline at end of file diff --git a/PettingZoo.Tapeti/TypeToJObjectConverter.cs b/PettingZoo.Tapeti/TypeToJObjectConverter.cs index 2bb652f..7efcd8e 100644 --- a/PettingZoo.Tapeti/TypeToJObjectConverter.cs +++ b/PettingZoo.Tapeti/TypeToJObjectConverter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace PettingZoo.Tapeti @@ -59,7 +60,20 @@ namespace PettingZoo.Tapeti actualType = equivalentType; - // TODO check for JsonConverter attribute? doubt we'll be able to generate a nice value for it, but at least we can provide a placeholder + try + { + if (type.GetCustomAttribute() != null) + { + // This type uses custom Json conversion so there's no way to know how to provide an example. + // We could try to create an instance of the type and pass it through the converter, but for now we'll + // just output a placeholder. + return ""; + } + } + catch + { + // Move along + } // String is also a class if (actualType == typeof(string)) diff --git a/PettingZoo.Tapeti/UI/PackageProgress/PackageProgressWindow.xaml b/PettingZoo.Tapeti/UI/PackageProgress/PackageProgressWindow.xaml deleted file mode 100644 index 45ccb52..0000000 --- a/PettingZoo.Tapeti/UI/PackageProgress/PackageProgressWindow.xaml +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/PettingZoo.Tapeti/UI/PackageProgress/PackageProgressWindow.xaml.cs b/PettingZoo.Tapeti/UI/PackageProgress/PackageProgressWindow.xaml.cs deleted file mode 100644 index cf8e15b..0000000 --- a/PettingZoo.Tapeti/UI/PackageProgress/PackageProgressWindow.xaml.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace PettingZoo.Tapeti.UI.PackageProgress -{ - /// - /// Interaction logic for PackageProgressWindow.xaml - /// - public partial class PackageProgressWindow : IProgress - { - public PackageProgressWindow() - { - InitializeComponent(); - } - - - public void Report(int value) - { - Dispatcher.BeginInvoke(() => - { - Progress.Value = value; - }); - } - } -} diff --git a/PettingZoo.Tapeti/UI/PackageSelection/PackageSelectionViewModel.cs b/PettingZoo.Tapeti/UI/PackageSelection/PackageSelectionViewModel.cs index 2e0adc4..f690090 100644 --- a/PettingZoo.Tapeti/UI/PackageSelection/PackageSelectionViewModel.cs +++ b/PettingZoo.Tapeti/UI/PackageSelection/PackageSelectionViewModel.cs @@ -88,7 +88,6 @@ namespace PettingZoo.Tapeti.UI.PackageSelection public ICommand AssemblyBrowse => assemblyBrowse; - // TODO hint for extra assemblies path public static string HintNuGetSources => string.Format(PackageSelectionStrings.HintNuGetSources, PettingZooPaths.InstallationRoot, PettingZooPaths.AppDataRoot); public string NuGetSearchTerm diff --git a/PettingZoo.WPF/PettingZoo.WPF.csproj b/PettingZoo.WPF/PettingZoo.WPF.csproj index 836a73c..3adc030 100644 --- a/PettingZoo.WPF/PettingZoo.WPF.csproj +++ b/PettingZoo.WPF/PettingZoo.WPF.csproj @@ -17,6 +17,28 @@ + + ProgressStrings.resx + True + True + + + Code + + + + + + ProgressStrings.Designer.cs + PublicResXFileCodeGenerator + + + + + + $(DefaultXamlRuntime) + Designer + $(DefaultXamlRuntime) diff --git a/PettingZoo.Tapeti/UI/PackageProgress/PackageProgressStrings.Designer.cs b/PettingZoo.WPF/ProgressWindow/ProgressStrings.Designer.cs similarity index 84% rename from PettingZoo.Tapeti/UI/PackageProgress/PackageProgressStrings.Designer.cs rename to PettingZoo.WPF/ProgressWindow/ProgressStrings.Designer.cs index e0b38ff..3cb953b 100644 --- a/PettingZoo.Tapeti/UI/PackageProgress/PackageProgressStrings.Designer.cs +++ b/PettingZoo.WPF/ProgressWindow/ProgressStrings.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace PettingZoo.Tapeti.UI.PackageProgress { +namespace PettingZoo.WPF.ProgressWindow { using System; @@ -22,14 +22,14 @@ namespace PettingZoo.Tapeti.UI.PackageProgress { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class PackageProgressStrings { + public class ProgressStrings { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal PackageProgressStrings() { + internal ProgressStrings() { } /// @@ -39,7 +39,7 @@ namespace PettingZoo.Tapeti.UI.PackageProgress { public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.Tapeti.UI.PackageProgress.PackageProgressStrings", typeof(PackageProgressStrings).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.WPF.ProgressWindow.ProgressStrings", typeof(ProgressStrings).Assembly); resourceMan = temp; } return resourceMan; @@ -61,11 +61,11 @@ namespace PettingZoo.Tapeti.UI.PackageProgress { } /// - /// Looks up a localized string similar to Reading message classes.... + /// Looks up a localized string similar to Cancel. /// - public static string WindowTitle { + public static string ButtonCancel { get { - return ResourceManager.GetString("WindowTitle", resourceCulture); + return ResourceManager.GetString("ButtonCancel", resourceCulture); } } } diff --git a/PettingZoo.WPF/ProgressWindow/ProgressStrings.resx b/PettingZoo.WPF/ProgressWindow/ProgressStrings.resx new file mode 100644 index 0000000..e527efc --- /dev/null +++ b/PettingZoo.WPF/ProgressWindow/ProgressStrings.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Cancel + + \ No newline at end of file diff --git a/PettingZoo.WPF/ProgressWindow/ProgressWindow.xaml b/PettingZoo.WPF/ProgressWindow/ProgressWindow.xaml new file mode 100644 index 0000000..029fae9 --- /dev/null +++ b/PettingZoo.WPF/ProgressWindow/ProgressWindow.xaml @@ -0,0 +1,16 @@ + + + +