diff --git a/PettingZoo.Core/PettingZoo.Core.csproj b/PettingZoo.Core/PettingZoo.Core.csproj index e37010b..6302265 100644 --- a/PettingZoo.Core/PettingZoo.Core.csproj +++ b/PettingZoo.Core/PettingZoo.Core.csproj @@ -8,6 +8,7 @@ + diff --git a/PettingZoo.Core/Settings/PettingZooPaths.cs b/PettingZoo.Core/Settings/PettingZooPaths.cs index 0b52081..b432e3b 100644 --- a/PettingZoo.Core/Settings/PettingZooPaths.cs +++ b/PettingZoo.Core/Settings/PettingZooPaths.cs @@ -9,6 +9,16 @@ namespace PettingZoo.Core.Settings public static string AppDataRoot { get; } public static string InstallationRoot { get; } + public static string LogPath => Path.Combine(AppDataRoot, @"Logs"); + + public static string DatabasePath => AppDataRoot; + + + public const string AssembliesPath = @"Assemblies"; + + public static string AppDataAssemblies => Path.Combine(AppDataRoot, AssembliesPath); + public static string InstallationAssemblies => Path.Combine(InstallationRoot, AssembliesPath); + static PettingZooPaths() { diff --git a/PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj b/PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj index 7a65453..45d6771 100644 --- a/PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj +++ b/PettingZoo.RabbitMQ/PettingZoo.RabbitMQ.csproj @@ -9,6 +9,7 @@ + diff --git a/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs b/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs index aebb52b..8a605e6 100644 --- a/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs +++ b/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs @@ -16,7 +16,7 @@ namespace PettingZoo.Settings.LiteDB public BaseLiteDBRepository(string databaseName) { - databaseFilename = Path.Combine(PettingZooPaths.AppDataRoot, $"{databaseName}.litedb"); + databaseFilename = Path.Combine(PettingZooPaths.DatabasePath, $"{databaseName}.litedb"); } diff --git a/PettingZoo.Settings.LiteDB/PettingZoo.Settings.LiteDB.csproj b/PettingZoo.Settings.LiteDB/PettingZoo.Settings.LiteDB.csproj index d6532ad..f5fef95 100644 --- a/PettingZoo.Settings.LiteDB/PettingZoo.Settings.LiteDB.csproj +++ b/PettingZoo.Settings.LiteDB/PettingZoo.Settings.LiteDB.csproj @@ -9,6 +9,7 @@ + diff --git a/PettingZoo.Tapeti/AssemblyParser/AssemblyParser.cs b/PettingZoo.Tapeti/AssemblyParser/AssemblyParser.cs index 6adb833..b7a5072 100644 --- a/PettingZoo.Tapeti/AssemblyParser/AssemblyParser.cs +++ b/PettingZoo.Tapeti/AssemblyParser/AssemblyParser.cs @@ -2,37 +2,37 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; +using System.Runtime.Loader; using Newtonsoft.Json; using PettingZoo.Core.Generator; -using Tapeti.DataAnnotations.Extensions; namespace PettingZoo.Tapeti.AssemblyParser { public class AssemblyParser : IDisposable { - private readonly MetadataLoadContext loadContext; + private readonly AssemblyLoadContext loadContext; - public AssemblyParser(params string[] extraAssemblies) + public AssemblyParser(params string[] extraAssembliesPaths) { - var runtimeAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll"); - var paths = runtimeAssemblies - .Concat(extraAssemblies) + // Using the MetadataLoadContext introduces extra complexity since types can not be compared + // (a string from the loaded assembly does not equal our typeof(string) for example). + // So instead we'll use a regular AssemblyLoadContext. Not ideal, and will probably cause other side-effects + // if we're not careful, but I don't feel like writing a full metadata parser right now. + // If you have a better idea, it's open-source! :-) + loadContext = new AssemblyLoadContext(null, true); - // TODO find a cleaner way - .Append(typeof(JsonSerializer).Assembly.Location) - .Append(typeof(RequiredGuidAttribute).Assembly.Location); - - - var resolver = new PathAssemblyResolver(paths); - loadContext = new MetadataLoadContext(resolver); + foreach (var extraAssembly in extraAssembliesPaths.SelectMany(p => Directory.Exists(p) + ? Directory.GetFiles(p, "*.dll") + : Enumerable.Empty())) + { + loadContext.LoadFromAssemblyPath(extraAssembly); + } } public void Dispose() { - loadContext.Dispose(); + loadContext.Unload(); GC.SuppressFinalize(this); } diff --git a/PettingZoo.Tapeti/NuGet/INuGetPackageManager.cs b/PettingZoo.Tapeti/NuGet/INuGetPackageManager.cs index 9ac0f22..a7a7880 100644 --- a/PettingZoo.Tapeti/NuGet/INuGetPackageManager.cs +++ b/PettingZoo.Tapeti/NuGet/INuGetPackageManager.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; namespace PettingZoo.Tapeti.NuGet { - // TODO support logger public interface INuGetPackageManager { public IReadOnlyList Sources { get; } @@ -36,7 +35,6 @@ namespace PettingZoo.Tapeti.NuGet { public string Version { get; } - // TODO support fetching dependencies public Task Download(Stream destination, CancellationToken cancellationToken); } } diff --git a/PettingZoo.Tapeti/NuGet/NuGetPackageManager.cs b/PettingZoo.Tapeti/NuGet/NuGetPackageManager.cs index f618dc2..def5e42 100644 --- a/PettingZoo.Tapeti/NuGet/NuGetPackageManager.cs +++ b/PettingZoo.Tapeti/NuGet/NuGetPackageManager.cs @@ -9,23 +9,28 @@ using NuGet.Common; using NuGet.Protocol; using NuGet.Protocol.Core.Types; using NuGet.Versioning; +using ILogger = Serilog.ILogger; namespace PettingZoo.Tapeti.NuGet { public class NuGetPackageManager : INuGetPackageManager { + private const string NuGetDefaultSource = @"https://api.nuget.org/v3/index.json"; + + private readonly ILogger logger; private readonly SourceCacheContext cache; private readonly List sources; public IReadOnlyList Sources => sources; - public NuGetPackageManager() + public NuGetPackageManager(ILogger logger) { + this.logger = logger; cache = new SourceCacheContext(); sources = new List { - new(cache, "nuget.org", @"https://api.nuget.org/v3/index.json") + new(logger.ForContext("source", NuGetDefaultSource), cache, "nuget.org", NuGetDefaultSource) }; } @@ -53,7 +58,7 @@ namespace PettingZoo.Tapeti.NuGet if (string.IsNullOrEmpty(nameAttribute?.Value) || string.IsNullOrEmpty(urlAttribute?.Value)) continue; - sources.Add(new Source(cache, nameAttribute.Value, urlAttribute.Value)); + sources.Add(new Source(logger.ForContext("source", urlAttribute.Value), cache, nameAttribute.Value, urlAttribute.Value)); } return this; @@ -63,14 +68,16 @@ namespace PettingZoo.Tapeti.NuGet private class Source : INuGetPackageSource { + private readonly ILogger logger; private readonly SourceCacheContext cache; private readonly SourceRepository repository; public string Name { get; } - public Source(SourceCacheContext cache, string name, string url) + public Source(ILogger logger, SourceCacheContext cache, string name, string url) { + this.logger = logger; this.cache = cache; Name = name; repository = Repository.Factory.GetCoreV3(url); @@ -82,19 +89,30 @@ namespace PettingZoo.Tapeti.NuGet if (string.IsNullOrWhiteSpace(searchTerm)) return Array.Empty(); - var resource = await repository.GetResourceAsync(cancellationToken); - var filter = new SearchFilter(includePrerelease); + try + { + var resource = await repository.GetResourceAsync(cancellationToken); + var filter = new SearchFilter(includePrerelease); - return (await resource.SearchAsync(searchTerm, filter, 0, 20, new NullLogger(), - cancellationToken)) - .Select(p => new Package(cache, repository, p)) - .ToArray(); + var result = (await resource.SearchAsync(searchTerm, filter, 0, 20, new NullLogger(), + cancellationToken)) + .Select(p => new Package(logger, cache, repository, p)) + .ToArray(); + + return result; + } + catch (Exception e) + { + logger.Error(e, "NuGet Search failed for term '{searchTerm}' (includePrerelease {includePrerelease})", searchTerm, includePrerelease); + throw; + } } } protected class Package : INuGetPackage { + private readonly ILogger logger; private readonly SourceCacheContext cache; private readonly SourceRepository repository; private readonly IPackageSearchMetadata packageSearchMetadata; @@ -108,8 +126,9 @@ namespace PettingZoo.Tapeti.NuGet private IReadOnlyList? versions; - public Package(SourceCacheContext cache, SourceRepository repository, IPackageSearchMetadata packageSearchMetadata) + public Package(ILogger logger, SourceCacheContext cache, SourceRepository repository, IPackageSearchMetadata packageSearchMetadata) { + this.logger = logger; this.cache = cache; this.repository = repository; this.packageSearchMetadata = packageSearchMetadata; @@ -118,9 +137,17 @@ namespace PettingZoo.Tapeti.NuGet public async Task> GetVersions(CancellationToken cancellationToken) { - return versions ??= (await packageSearchMetadata.GetVersionsAsync()) - .Select(v => new PackageVersion(cache, repository, packageSearchMetadata, v.Version)) - .ToArray(); + try + { + return versions ??= (await packageSearchMetadata.GetVersionsAsync()) + .Select(v => new PackageVersion(cache, repository, packageSearchMetadata, v.Version)) + .ToArray(); + } + catch (Exception e) + { + logger.Error(e, "NuGet GetVersions failed for packge Id '{packageId}')", packageSearchMetadata.Identity.Id); + throw; + } } } diff --git a/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj b/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj index dbce67c..8234b3e 100644 --- a/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj +++ b/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj @@ -11,6 +11,7 @@ + diff --git a/PettingZoo.Tapeti/TapetiClassLibraryExampleGenerator.cs b/PettingZoo.Tapeti/TapetiClassLibraryExampleGenerator.cs index 2b1acdd..e34ad5d 100644 --- a/PettingZoo.Tapeti/TapetiClassLibraryExampleGenerator.cs +++ b/PettingZoo.Tapeti/TapetiClassLibraryExampleGenerator.cs @@ -13,14 +13,24 @@ using PettingZoo.Tapeti.NuGet; using PettingZoo.Tapeti.UI.ClassSelection; using PettingZoo.Tapeti.UI.PackageProgress; using PettingZoo.Tapeti.UI.PackageSelection; +using Serilog; namespace PettingZoo.Tapeti { public class TapetiClassLibraryExampleGenerator : IExampleGenerator { + private readonly ILogger logger; + + + public TapetiClassLibraryExampleGenerator(ILogger logger) + { + this.logger = logger; + } + + public void Select(object? ownerWindow, Action onExampleSelected) { - var packageManager = new NuGetPackageManager() + var packageManager = new NuGetPackageManager(logger) .WithSourcesFrom(Path.Combine(PettingZooPaths.InstallationRoot, @"nuget.config")) .WithSourcesFrom(Path.Combine(PettingZooPaths.AppDataRoot, @"nuget.config")); @@ -97,8 +107,11 @@ namespace PettingZoo.Tapeti private static IEnumerable LoadExamples(IEnumerable assemblies) { - // TODO support folder with additional assemblies to load, or implement a proper NuGet install (with dependencies) instead - var assemblyParser = new AssemblyParser.AssemblyParser(); + var assemblyParser = new AssemblyParser.AssemblyParser( + PettingZooPaths.AppDataAssemblies, + PettingZooPaths.InstallationAssemblies + ); + return assemblies .SelectMany(a => { diff --git a/PettingZoo.Tapeti/TypeToJObjectConverter.cs b/PettingZoo.Tapeti/TypeToJObjectConverter.cs index 865f8bc..5742475 100644 --- a/PettingZoo.Tapeti/TypeToJObjectConverter.cs +++ b/PettingZoo.Tapeti/TypeToJObjectConverter.cs @@ -6,26 +6,47 @@ using Newtonsoft.Json.Linq; namespace PettingZoo.Tapeti { - // TODO detect recursion - // TODO detect recursion - // TODO detect recursion - // TODO stop making nerdy jokes in comments. - - // TODO generate at least one item for enumerables - // TODO support basic types - public class TypeToJObjectConverter { public static JObject Convert(Type type) { + if (!type.IsClass) + throw new ArgumentException($"TypeToJObjectConverter.Convert expects a class, got {type.Name}"); + + return ClassToJToken(type, Array.Empty()); + } + + + private static readonly Dictionary TypeEquivalenceMap = new() + { + { typeof(uint), typeof(int) }, + { typeof(long), typeof(int) }, + { typeof(ulong), typeof(int) }, + { typeof(short), typeof(int) }, + { typeof(ushort), typeof(int) }, + { typeof(float), typeof(decimal) } + }; + + + private static readonly Dictionary TypeValueMap = new() + { + { typeof(int), 0 }, + { typeof(decimal), 0.0 }, + { typeof(bool), false } + }; + + + private static JObject ClassToJToken(Type classType, IEnumerable typesEncountered) + { + var newTypesEncountered = typesEncountered.Append(classType).ToArray(); var result = new JObject(); - foreach (var propertyInfo in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + foreach (var propertyInfo in classType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { - // Note: unfortunately we can not call GetCustomAttributes here, as that would - // trigger assemblies not included in the package to be loaded + // Note: unfortunately we can not call GetCustomAttributes here for now, as that would + // trigger assemblies not included in the package to be loaded, which may not exist - var value = PropertyToJToken(propertyInfo.PropertyType); + var value = TypeToJToken(propertyInfo.PropertyType, newTypesEncountered); result.Add(propertyInfo.Name, value); } @@ -33,23 +54,12 @@ namespace PettingZoo.Tapeti } - private static readonly Dictionary TypeMap = new() + private static JToken TypeToJToken(Type type, ICollection typesEncountered) { - { typeof(short), 0 }, - { typeof(ushort), 0 }, - { typeof(int), 0 }, - { typeof(uint), 0 }, - { typeof(long), 0 }, - { typeof(ulong), 0 }, - { typeof(decimal), 0.0 }, - { typeof(float), 0.0 }, - { typeof(bool), false } - }; + var actualType = Nullable.GetUnderlyingType(type) ?? type; - - private static JToken PropertyToJToken(Type propertyType) - { - var actualType = Nullable.GetUnderlyingType(propertyType) ?? propertyType; + if (TypeEquivalenceMap.TryGetValue(actualType, out var equivalentType)) + actualType = equivalentType; // String is also a class @@ -64,14 +74,13 @@ namespace PettingZoo.Tapeti .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); if (enumerableInterface != null) - return new JArray(Convert(enumerableInterface.GetGenericArguments()[0])); + return new JArray(TypeToJToken(enumerableInterface.GetGenericArguments()[0], typesEncountered)); - - return Convert(actualType); + return typesEncountered.Contains(actualType) ? new JValue((object?)null) : ClassToJToken(actualType, typesEncountered); } if (actualType.IsArray) - return new JArray(Convert(actualType.GetElementType()!)); + return new JArray(TypeToJToken(actualType.GetElementType()!, typesEncountered)); if (actualType.IsEnum) return Enum.GetNames(actualType).FirstOrDefault(); @@ -88,11 +97,9 @@ namespace PettingZoo.Tapeti if (actualType == typeof(Guid)) return Guid.NewGuid().ToString(); - return TypeMap.TryGetValue(actualType, out var mappedToken) + return TypeValueMap.TryGetValue(actualType, out var mappedToken) ? mappedToken : $"(unknown type: {actualType.Name})"; } - - } } diff --git a/PettingZoo.Tapeti/UI/PackageSelection/PackageSelectionViewModel.cs b/PettingZoo.Tapeti/UI/PackageSelection/PackageSelectionViewModel.cs index f690090..2e0adc4 100644 --- a/PettingZoo.Tapeti/UI/PackageSelection/PackageSelectionViewModel.cs +++ b/PettingZoo.Tapeti/UI/PackageSelection/PackageSelectionViewModel.cs @@ -88,6 +88,7 @@ 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.Test/PettingZoo.Test.csproj b/PettingZoo.Test/PettingZoo.Test.csproj new file mode 100644 index 0000000..cf3907a --- /dev/null +++ b/PettingZoo.Test/PettingZoo.Test.csproj @@ -0,0 +1,24 @@ + + + + net6.0-windows + enable + enable + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/PettingZoo.Test/Tapeti/TypeToJObjectTest.cs b/PettingZoo.Test/Tapeti/TypeToJObjectTest.cs new file mode 100644 index 0000000..a02dcb0 --- /dev/null +++ b/PettingZoo.Test/Tapeti/TypeToJObjectTest.cs @@ -0,0 +1,65 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using FluentAssertions.Json; +using Newtonsoft.Json.Linq; +using PettingZoo.Tapeti; +using Xunit; + +#nullable disable + +namespace PettingZoo.Test.Tapeti +{ + public class TypeToJObjectTest + { + [Fact] + public void TestAllSupportedTypes() + { + var converted = TypeToJObjectConverter.Convert(typeof(AllSupportedTypesTest)); + + using (new AssertionScope()) + { + // Directly supported + converted.Should().HaveElement("StringValue").Which.Should().HaveValue(""); + converted.Should().HaveElement("IntValue").Which.Should().Match(t => t.Type == JTokenType.Integer); + converted.Should().HaveElement("BoolValue").Which.Should().Match(t => t.Type == JTokenType.Boolean); + + var guidValue = converted.Should().HaveElement("GuidValue").Which.Should().Match(t => t.Type == JTokenType.String) + .And.Subject.Value(); + Guid.TryParse(guidValue, out _).Should().BeTrue(); + + + var objectValue = converted.Should().HaveElement("ObjectValue").Subject; + objectValue.Should().HaveElement("SubStringValue").Which.Should().HaveValue(""); + objectValue.Should().HaveElement("SubIntValue").Which.Should().Match(t => t.Type == JTokenType.Integer); + objectValue.Should().HaveElement("RecursiveValue").Which.Type.Should().Be(JTokenType.Null); + + // Via type mapping + // TODO + } + } + + + // ReSharper disable UnusedMember.Local + private class AllSupportedTypesTest + { + public string StringValue { get; set; } + public int IntValue { get; set; } + public bool BoolValue { get; set; } + + public Guid GuidValue { get; set; } + + public ClassProperty ObjectValue { get; set; } + } + + + // ReSharper disable once ClassNeverInstantiated.Local + private class ClassProperty + { + public string SubStringValue { get; set; } + public int SubIntValue { get; set; } + + public AllSupportedTypesTest RecursiveValue { get; set; } + } + // ReSharper restore UnusedMember.Local + } +} \ No newline at end of file diff --git a/PettingZoo.sln b/PettingZoo.sln index f2ad741..e840afc 100644 --- a/PettingZoo.sln +++ b/PettingZoo.sln @@ -18,7 +18,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.Tapeti", "Pettin EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.Settings.LiteDB", "PettingZoo.Settings.LiteDB\PettingZoo.Settings.LiteDB.csproj", "{7157B09C-FDD9-4928-B14D-C25B784CA865}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.WPF", "PettingZoo.WPF\PettingZoo.WPF.csproj", "{E6617B69-2AC4-4056-B801-DD32E2374B71}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.WPF", "PettingZoo.WPF\PettingZoo.WPF.csproj", "{E6617B69-2AC4-4056-B801-DD32E2374B71}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.Test", "PettingZoo.Test\PettingZoo.Test.csproj", "{3DD7F8D5-2CEE-414D-AC9C-9F395568B79F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -50,6 +52,10 @@ Global {E6617B69-2AC4-4056-B801-DD32E2374B71}.Debug|Any CPU.Build.0 = Debug|Any CPU {E6617B69-2AC4-4056-B801-DD32E2374B71}.Release|Any CPU.ActiveCfg = Release|Any CPU {E6617B69-2AC4-4056-B801-DD32E2374B71}.Release|Any CPU.Build.0 = Release|Any CPU + {3DD7F8D5-2CEE-414D-AC9C-9F395568B79F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3DD7F8D5-2CEE-414D-AC9C-9F395568B79F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DD7F8D5-2CEE-414D-AC9C-9F395568B79F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3DD7F8D5-2CEE-414D-AC9C-9F395568B79F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/PettingZoo/Program.cs b/PettingZoo/Program.cs index f251fa4..9ef7cd9 100644 --- a/PettingZoo/Program.cs +++ b/PettingZoo/Program.cs @@ -56,7 +56,7 @@ namespace PettingZoo private static ILogger CreateLogger() { - var logPath = Path.Combine(PettingZooPaths.AppDataRoot, @"logs", "PettingZoo.log"); + var logPath = Path.Combine(PettingZooPaths.LogPath, @"PettingZoo.log"); return new LoggerConfiguration() .MinimumLevel.Verbose() diff --git a/PettingZoo/TODO.md b/PettingZoo/TODO.md index 9657558..0aae027 100644 --- a/PettingZoo/TODO.md +++ b/PettingZoo/TODO.md @@ -1,13 +1,15 @@ Must-have --------- +- Check required fields before enabling Publish button Should-have ----------- - Save / load publisher messages (either as templates or to disk) -- Export received messages to Tapeti JSON file / Tapeti.Cmd command-line +- Tapeti: export received messages to Tapeti.Cmd JSON file / Tapeti.Cmd command-line +- Tapeti: fetch NuGet dependencies to improve the chances of succesfully loading the assembly, instead of the current "extraAssembliesPaths" workaround Nice-to-have ------------ -- JSON syntax highlighting \ No newline at end of file +- Validation against message classes (for Tapeti messages) \ No newline at end of file diff --git a/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewModel.cs b/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewModel.cs index 6a302de..75c76a3 100644 --- a/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewModel.cs +++ b/PettingZoo/UI/Tab/Publisher/TapetiPublisherViewModel.cs @@ -127,8 +127,6 @@ namespace PettingZoo.UI.Tab.Publisher } Payload = example.Generate(); - - // TODO if validating example, keep reference for validation... and implement validation of course }); }); }