diff --git a/PettingZoo.Core/Generator/IExampleGenerator.cs b/PettingZoo.Core/Generator/IExampleGenerator.cs index ae49c7a..95b0e95 100644 --- a/PettingZoo.Core/Generator/IExampleGenerator.cs +++ b/PettingZoo.Core/Generator/IExampleGenerator.cs @@ -1,25 +1,33 @@ using System; -using System.Collections.Generic; namespace PettingZoo.Core.Generator { - public interface IExampleSource : IDisposable + public interface IExampleGenerator { - IExampleFolder GetRootFolder(); + void Select(object? ownerWindow, Action onExampleSelected); } - public interface IExampleFolder - { - public string Name { get; } - - public IReadOnlyList Folders { get; } - public IReadOnlyList Messages { get; } - } - - - public interface IExampleMessage + public interface IExample { string Generate(); } + + + public interface IClassTypeExample : IExample + { + public string AssemblyName { get; } + public string? Namespace { get; } + public string ClassName { get; } + + public string FullClassName => !string.IsNullOrEmpty(Namespace) ? Namespace + "." : "" + ClassName; + } + + + /* + public interface IValidatingExample : IExample + { + bool Validate(string payload, out string validationMessage); + } + */ } diff --git a/PettingZoo.Core/Rendering/MessageBodyRenderer.cs b/PettingZoo.Core/Rendering/MessageBodyRenderer.cs index f3d4128..029be9c 100644 --- a/PettingZoo.Core/Rendering/MessageBodyRenderer.cs +++ b/PettingZoo.Core/Rendering/MessageBodyRenderer.cs @@ -7,7 +7,7 @@ namespace PettingZoo.Core.Rendering { public class MessageBodyRenderer { - public static Dictionary> ContentTypeHandlers = new() + private static readonly Dictionary> ContentTypeHandlers = new() { { "application/json", RenderJson } }; @@ -15,7 +15,7 @@ namespace PettingZoo.Core.Rendering public static string Render(byte[] body, string? contentType) { - return (contentType != null) && ContentTypeHandlers.TryGetValue(contentType, out var handler) + return contentType != null && ContentTypeHandlers.TryGetValue(contentType, out var handler) ? handler(body) : Encoding.UTF8.GetString(body); diff --git a/PettingZoo.Core/Settings/PettingZooPaths.cs b/PettingZoo.Core/Settings/PettingZooPaths.cs new file mode 100644 index 0000000..0b52081 --- /dev/null +++ b/PettingZoo.Core/Settings/PettingZooPaths.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; +using System.Reflection; + +namespace PettingZoo.Core.Settings +{ + public static class PettingZooPaths + { + public static string AppDataRoot { get; } + public static string InstallationRoot { get; } + + + static PettingZooPaths() + { + var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + if (appDataPath == null) + throw new IOException("Could not resolve application data path"); + + AppDataRoot = Path.Combine(appDataPath, @"PettingZoo"); + if (!Directory.CreateDirectory(AppDataRoot).Exists) + throw new IOException($"Failed to create directory: {AppDataRoot}"); + + InstallationRoot = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location ?? Assembly.GetExecutingAssembly().Location)!; + } + } +} diff --git a/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs b/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs index f255b99..aebb52b 100644 --- a/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs +++ b/PettingZoo.Settings.LiteDB/BaseLiteDBRepository.cs @@ -1,5 +1,6 @@ using LiteDB; using LiteDB.Async; +using PettingZoo.Core.Settings; namespace PettingZoo.Settings.LiteDB { @@ -15,15 +16,7 @@ namespace PettingZoo.Settings.LiteDB public BaseLiteDBRepository(string databaseName) { - var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - if (appDataPath == null) - throw new IOException("Could not resolve application data path"); - - var databasePath = Path.Combine(appDataPath, @"PettingZoo"); - if (!Directory.CreateDirectory(databasePath).Exists) - throw new IOException($"Failed to create directory: {databasePath}"); - - databaseFilename = Path.Combine(databasePath, $"{databaseName}.litedb"); + databaseFilename = Path.Combine(PettingZooPaths.AppDataRoot, $"{databaseName}.litedb"); } diff --git a/PettingZoo.Tapeti/AssemblyLoader/FilePackageAssemblies.cs b/PettingZoo.Tapeti/AssemblyLoader/FilePackageAssemblies.cs new file mode 100644 index 0000000..c62a8c3 --- /dev/null +++ b/PettingZoo.Tapeti/AssemblyLoader/FilePackageAssemblies.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace PettingZoo.Tapeti.AssemblyLoader +{ + public class FilePackageAssemblies : IPackageAssemblies + { + private readonly string[] filenames; + + + public FilePackageAssemblies(params string[] filenames) + { + this.filenames = filenames; + } + + + public Task> GetAssemblies(IProgress progress, CancellationToken cancellationToken) + { + return Task.FromResult(filenames.Select(f => (IPackageAssembly)new FilePackageAssembly(f))); + } + + + + private class FilePackageAssembly : IPackageAssembly + { + private readonly string filename; + + + public FilePackageAssembly(string filename) + { + this.filename = filename; + } + + + public Stream GetStream() + { + return new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + } + } + } +} diff --git a/PettingZoo.Tapeti/AssemblyLoader/IPackageAssemblies.cs b/PettingZoo.Tapeti/AssemblyLoader/IPackageAssemblies.cs new file mode 100644 index 0000000..e25d5f1 --- /dev/null +++ b/PettingZoo.Tapeti/AssemblyLoader/IPackageAssemblies.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace PettingZoo.Tapeti.AssemblyLoader +{ + public interface IPackageAssemblies + { + Task> GetAssemblies(IProgress progress, CancellationToken cancellationToken); + } + + + public interface IPackageAssembly + { + Stream GetStream(); + } +} diff --git a/PettingZoo.Tapeti/AssemblyLoader/NuGetPackageAssemblies.cs b/PettingZoo.Tapeti/AssemblyLoader/NuGetPackageAssemblies.cs new file mode 100644 index 0000000..0d98901 --- /dev/null +++ b/PettingZoo.Tapeti/AssemblyLoader/NuGetPackageAssemblies.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +using NuGet.Frameworks; +using NuGet.Packaging; +using PettingZoo.Tapeti.NuGet; + +namespace PettingZoo.Tapeti.AssemblyLoader +{ + public class NuGetPackageAssemblies : IPackageAssemblies + { + private readonly INuGetPackageVersion packageVersion; + + + public NuGetPackageAssemblies(INuGetPackageVersion packageVersion) + { + this.packageVersion = packageVersion; + } + + + public async Task> GetAssemblies(IProgress progress, CancellationToken cancellationToken) + { + await using var packageStream = new MemoryStream(); + await packageVersion.Download(packageStream, cancellationToken); + + packageStream.Seek(0, SeekOrigin.Begin); + using var packageReader = new PackageArchiveReader(packageStream); + + // Determine which frameworks versions PettingZoo is compatible with so that we can actually load the assemblies + var targetFrameworkAttribute = Assembly.GetExecutingAssembly() + .GetCustomAttributes(typeof(TargetFrameworkAttribute), false) + .Cast() + .Single(); + + var targetFramework = NuGetFramework.ParseFrameworkName(targetFrameworkAttribute.FrameworkName, DefaultFrameworkNameProvider.Instance); + + var libVersion = (await packageReader.GetLibItemsAsync(cancellationToken)) + .Where(l => DefaultCompatibilityProvider.Instance.IsCompatible(targetFramework, l.TargetFramework)) + .OrderByDescending(l => l.TargetFramework) + .FirstOrDefault(); + + if (libVersion == null) + return Enumerable.Empty(); + + + var assemblies = new List(); + + foreach (var filename in libVersion.Items.Where(f => f.EndsWith(@".dll", StringComparison.InvariantCultureIgnoreCase))) + { + var assembly = await new NuGetPackageAssembly().CopyFrom(packageReader.GetStream(filename)); + assemblies.Add(assembly); + } + + return assemblies; + } + + + + private class NuGetPackageAssembly : IPackageAssembly + { + private readonly MemoryStream buffer = new(); + + + public async Task CopyFrom(Stream stream) + { + await stream.CopyToAsync(buffer); + return this; + } + + + public Stream GetStream() + { + return new MemoryStream(buffer.GetBuffer()); + } + } + } +} diff --git a/PettingZoo.Tapeti/AssemblyParser/AssemblyParser.cs b/PettingZoo.Tapeti/AssemblyParser/AssemblyParser.cs new file mode 100644 index 0000000..6adb833 --- /dev/null +++ b/PettingZoo.Tapeti/AssemblyParser/AssemblyParser.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using Newtonsoft.Json; +using PettingZoo.Core.Generator; +using Tapeti.DataAnnotations.Extensions; + +namespace PettingZoo.Tapeti.AssemblyParser +{ + public class AssemblyParser : IDisposable + { + private readonly MetadataLoadContext loadContext; + + public AssemblyParser(params string[] extraAssemblies) + { + var runtimeAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll"); + var paths = runtimeAssemblies + .Concat(extraAssemblies) + + // TODO find a cleaner way + .Append(typeof(JsonSerializer).Assembly.Location) + .Append(typeof(RequiredGuidAttribute).Assembly.Location); + + + var resolver = new PathAssemblyResolver(paths); + loadContext = new MetadataLoadContext(resolver); + } + + + public void Dispose() + { + loadContext.Dispose(); + GC.SuppressFinalize(this); + } + + + public IEnumerable GetExamples(Stream assemblyStream) + { + var assembly = loadContext.LoadFromStream(assemblyStream); + + foreach (var type in assembly.GetTypes().Where(t => t.IsClass)) + yield return new TypeExample(type); + } + + + + private class TypeExample : IClassTypeExample + { + public string AssemblyName => type.Assembly.GetName().Name ?? ""; + public string? Namespace => type.Namespace; + public string ClassName => type.Name; + + private readonly Type type; + + + public TypeExample(Type type) + { + this.type = type; + } + + + public string Generate() + { + /* + We can't create an instance of the type to serialize easily, as most will depend on + assemblies not included in the NuGet package, so we'll parse the Type ourselves. + This is still much easier than using MetadataReader, as we can more easily check against + standard types like Nullable. + + The only external dependencies should be the attributes, like [RequiredGuid]. The messaging models + themselves should not inherit from classes outside of their assembly, or include properties + with types from other assemblies. With that assumption, walking the class structure should be safe. + The extraAssemblies passed to TapetiClassLibraryExampleSource can also be used to give it a better chance. + */ + var serialized = TypeToJObjectConverter.Convert(type); + return serialized.ToString(Formatting.Indented); + } + } + } +} diff --git a/PettingZoo.Tapeti/NuGet/INuGetPackageManager.cs b/PettingZoo.Tapeti/NuGet/INuGetPackageManager.cs new file mode 100644 index 0000000..9ac0f22 --- /dev/null +++ b/PettingZoo.Tapeti/NuGet/INuGetPackageManager.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace PettingZoo.Tapeti.NuGet +{ + // TODO support logger + public interface INuGetPackageManager + { + public IReadOnlyList Sources { get; } + } + + + public interface INuGetPackageSource + { + public string Name { get; } + + public Task> Search(string searchTerm, bool includePrerelease, CancellationToken cancellationToken); + } + + + public interface INuGetPackage + { + public string Title { get; } + public string Description { get; } + public string Authors { get; } + public string Version { get; } + + public Task> GetVersions(CancellationToken cancellationToken); + } + + + public interface INuGetPackageVersion : IComparable + { + 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 new file mode 100644 index 0000000..f618dc2 --- /dev/null +++ b/PettingZoo.Tapeti/NuGet/NuGetPackageManager.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using NuGet.Common; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +namespace PettingZoo.Tapeti.NuGet +{ + public class NuGetPackageManager : INuGetPackageManager + { + private readonly SourceCacheContext cache; + private readonly List sources; + + public IReadOnlyList Sources => sources; + + + public NuGetPackageManager() + { + cache = new SourceCacheContext(); + sources = new List + { + new(cache, "nuget.org", @"https://api.nuget.org/v3/index.json") + }; + } + + + public NuGetPackageManager WithSourcesFrom(string nuGetConfig) + { + if (!File.Exists(nuGetConfig)) + return this; + + var doc = new XmlDocument(); + doc.Load(nuGetConfig); + + var nodes = doc.SelectNodes(@"/configuration/packageSources/add"); + if (nodes == null) + return this; + + foreach (var entry in nodes.Cast()) + { + if (entry.Attributes == null) + continue; + + var nameAttribute = entry.Attributes["key"]; + var urlAttribute = entry.Attributes["value"]; + + if (string.IsNullOrEmpty(nameAttribute?.Value) || string.IsNullOrEmpty(urlAttribute?.Value)) + continue; + + sources.Add(new Source(cache, nameAttribute.Value, urlAttribute.Value)); + } + + return this; + } + + + + private class Source : INuGetPackageSource + { + private readonly SourceCacheContext cache; + private readonly SourceRepository repository; + + public string Name { get; } + + + public Source(SourceCacheContext cache, string name, string url) + { + this.cache = cache; + Name = name; + repository = Repository.Factory.GetCoreV3(url); + } + + + public async Task> Search(string searchTerm, bool includePrerelease, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(searchTerm)) + return Array.Empty(); + + 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(); + } + } + + + protected class Package : INuGetPackage + { + private readonly SourceCacheContext cache; + private readonly SourceRepository repository; + private readonly IPackageSearchMetadata packageSearchMetadata; + + public string Title => packageSearchMetadata.Title; + public string Description => packageSearchMetadata.Description; + public string Authors => packageSearchMetadata.Authors; + public string Version => packageSearchMetadata.Identity.Version.ToString(); + + + private IReadOnlyList? versions; + + + public Package(SourceCacheContext cache, SourceRepository repository, IPackageSearchMetadata packageSearchMetadata) + { + this.cache = cache; + this.repository = repository; + this.packageSearchMetadata = packageSearchMetadata; + } + + + public async Task> GetVersions(CancellationToken cancellationToken) + { + return versions ??= (await packageSearchMetadata.GetVersionsAsync()) + .Select(v => new PackageVersion(cache, repository, packageSearchMetadata, v.Version)) + .ToArray(); + } + } + + + protected class PackageVersion : INuGetPackageVersion, IComparable + { + private readonly SourceCacheContext cache; + private readonly SourceRepository repository; + private readonly IPackageSearchMetadata packageSearchMetadata; + + protected readonly NuGetVersion NuGetVersion; + + + public PackageVersion(SourceCacheContext cache, SourceRepository repository, IPackageSearchMetadata packageSearchMetadata, NuGetVersion nuGetVersion) + { + this.cache = cache; + this.repository = repository; + this.packageSearchMetadata = packageSearchMetadata; + NuGetVersion = nuGetVersion; + } + + + public string Version => NuGetVersion.ToString(); + + + public async Task Download(Stream destination, CancellationToken cancellationToken) + { + var resource = await repository.GetResourceAsync(cancellationToken); + await resource.CopyNupkgToStreamAsync(packageSearchMetadata.Identity.Id, NuGetVersion, destination, cache, new NullLogger(), cancellationToken); + } + + + public int CompareTo(INuGetPackageVersion? other) + { + if (ReferenceEquals(this, other)) return 0; + if (other == null) return 1; + + return other is PackageVersion packageVersion ? CompareTo(packageVersion) : string.Compare(Version, other.Version, StringComparison.Ordinal); + } + + public int CompareTo(PackageVersion? other) + { + if (ReferenceEquals(this, other)) return 0; + return other == null ? 1 : NuGetVersion.CompareTo(other.NuGetVersion); + } + } + } +} diff --git a/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj b/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj index d9b83bb..dbce67c 100644 --- a/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj +++ b/PettingZoo.Tapeti/PettingZoo.Tapeti.csproj @@ -1,16 +1,69 @@ - + - net6.0 + net6.0-windows 0.1 + true + enable + + + + + + + + + + + + + True + True + ClassSelectionStrings.resx + + + True + True + PackageProgressStrings.resx + + + Code + + + True + True + PackageSelectionStrings.resx + + + + + + PublicResXFileCodeGenerator + ClassSelectionStrings.Designer.cs + + + PublicResXFileCodeGenerator + PackageProgressStrings.Designer.cs + + + PublicResXFileCodeGenerator + PackageSelectionStrings.Designer.cs + + + + + + $(DefaultXamlRuntime) + Designer + diff --git a/PettingZoo.Tapeti/TapetiClassLibraryExampleGenerator.cs b/PettingZoo.Tapeti/TapetiClassLibraryExampleGenerator.cs new file mode 100644 index 0000000..2b1acdd --- /dev/null +++ b/PettingZoo.Tapeti/TapetiClassLibraryExampleGenerator.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using PettingZoo.Core.Generator; +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; + +namespace PettingZoo.Tapeti +{ + public class TapetiClassLibraryExampleGenerator : IExampleGenerator + { + public void Select(object? ownerWindow, Action onExampleSelected) + { + var packageManager = new NuGetPackageManager() + .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) + { + Owner = ownerWindow as Window + }; + + viewModel.Select += (_, args) => + { + dispatcher.Invoke(() => + { + var windowBounds = selectionWindow.RestoreBounds; + selectionWindow.Close(); + + var progressWindow = new PackageProgressWindow(); + progressWindow.Left = windowBounds.Left + (windowBounds.Width - progressWindow.Width) / 2; + progressWindow.Left = windowBounds.Top + (windowBounds.Height - progressWindow.Height) / 2; + progressWindow.Show(); + + 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 classes = + var examples = LoadExamples(assemblies); + + dispatcher.Invoke(() => + { + progressWindow.Close(); + progressWindow = null; + + var classSelectionViewModel = new ClassSelectionViewModel(examples); + var classSelectionWindow = new ClassSelectionWindow(classSelectionViewModel) + { + Top = windowBounds.Top, + Left = windowBounds.Left, + Width = windowBounds.Width, + Height = windowBounds.Height + }; + + classSelectionViewModel.Select += (_, example) => + { + classSelectionWindow.Close(); + onExampleSelected(example); + }; + + classSelectionWindow.ShowDialog(); + }); + } + catch (Exception e) + { + 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); + }); + } + }); + }); + }; + + selectionWindow.ShowDialog(); + } + + + 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(); + return assemblies + .SelectMany(a => + { + using var stream = a.GetStream(); + return assemblyParser.GetExamples(stream).ToArray(); + }) + .ToArray(); + } + } +} diff --git a/PettingZoo.Tapeti/TapetiClassLibraryExampleSource.cs b/PettingZoo.Tapeti/TapetiClassLibraryExampleSource.cs deleted file mode 100644 index ec91428..0000000 --- a/PettingZoo.Tapeti/TapetiClassLibraryExampleSource.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; -using Newtonsoft.Json; -using PettingZoo.Core.Generator; - -namespace PettingZoo.Tapeti -{ - public class TapetiClassLibraryExampleSource : IExampleSource - { - private readonly string classLibraryFilename; - private readonly IEnumerable extraAssemblies; - private Lazy assemblySource; - - - public TapetiClassLibraryExampleSource(string classLibraryFilename, IEnumerable extraAssemblies) - { - this.classLibraryFilename = classLibraryFilename; - this.extraAssemblies = extraAssemblies; - - assemblySource = new Lazy(AssemblySourceFactory); - } - - - public void Dispose() - { - if (assemblySource.IsValueCreated) - assemblySource.Value.Dispose(); - - GC.SuppressFinalize(this); - } - - - public IExampleFolder GetRootFolder() - { - return assemblySource.Value.RootFolder; - } - - - private AssemblySource AssemblySourceFactory() - { - var runtimeAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll"); - - var paths = runtimeAssemblies - .Concat(extraAssemblies) - .Append(classLibraryFilename); - - // TODO can we use a custom resolver to detect missing references? - var resolver = new PathAssemblyResolver(paths); - var loadContext = new MetadataLoadContext(resolver); - try - { - var assembly = loadContext.LoadFromAssemblyPath(classLibraryFilename); - var rootFolder = new Folder(@"Root"); - - - foreach (var assemblyType in assembly.GetTypes()) - AddType(assemblyType, rootFolder); - - - return new AssemblySource - { - LoadContext = loadContext, - RootFolder = rootFolder - }; - } - catch - { - loadContext.Dispose(); - throw; - } - } - - - private void AddType(Type type, Folder rootFolder) - { - if (!type.IsClass) - return; - - var assemblyName = type.Assembly.GetName().Name + "."; - var typeNamespace = type.Namespace ?? ""; - - if (typeNamespace.StartsWith(assemblyName)) - typeNamespace = typeNamespace.Substring(assemblyName.Length); - - var folder = CreateFolder(rootFolder, typeNamespace); - folder.AddMessage(new Message(type)); - } - - - private static Folder CreateFolder(Folder rootFolder, string typeNamespace) - { - var parts = typeNamespace.Split('.'); - if (parts.Length == 0) - return rootFolder; - - var folder = rootFolder; - - foreach (var part in parts) - folder = folder.CreateFolder(part); - - return folder; - } - - - private class Folder : IExampleFolder - { - private readonly List folders = new(); - private readonly List messages = new(); - - - public string Name { get; } - public IReadOnlyList Folders => folders; - public IReadOnlyList Messages => messages; - - - public Folder(string name) - { - Name = name; - } - - - public Folder CreateFolder(string name) - { - var folder = folders.FirstOrDefault(f => f.Name == name); - if (folder != null) - return folder; - - folder = new Folder(name); - folders.Add(folder); - return folder; - } - - - public void AddMessage(IExampleMessage message) - { - messages.Add(message); - } - } - - - private class Message : IExampleMessage - { - private readonly Type type; - - - public Message(Type type) - { - this.type = type; - } - - - public string Generate() - { - /* - We can't create an instance of the type to serialize easily, as most will depend on - assemblies not included in the NuGet package, so we'll parse the Type ourselves. - This is still much easier than using MetadataReader, as we can more easily check against - standard types like Nullable. - - The only external dependencies should be the attributes, like [RequiredGuid]. The messaging models - themselves should not inherit from classes outside of their assembly, or include properties - with types from other assemblies. With that assumption, walking the class structure should be safe. - The extraAssemblies passed to TapetiClassLibraryExampleSource can also be used to give it a better chance. - */ - var serialized = TypeToJObjectConverter.Convert(type); - return serialized.ToString(Formatting.Indented); - } - } - - - private class AssemblySource : IDisposable - { - public MetadataLoadContext LoadContext { get; init; } - public IExampleFolder RootFolder { get; init; } - - - public void Dispose() - { - LoadContext.Dispose(); - } - } - } -} diff --git a/PettingZoo.Tapeti/TypeToJObjectConverter.cs b/PettingZoo.Tapeti/TypeToJObjectConverter.cs index d807501..865f8bc 100644 --- a/PettingZoo.Tapeti/TypeToJObjectConverter.cs +++ b/PettingZoo.Tapeti/TypeToJObjectConverter.cs @@ -6,7 +6,15 @@ using Newtonsoft.Json.Linq; namespace PettingZoo.Tapeti { - internal class TypeToJObjectConverter + // 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) { @@ -63,7 +71,7 @@ namespace PettingZoo.Tapeti } if (actualType.IsArray) - return new JArray(Convert(actualType.GetElementType())); + return new JArray(Convert(actualType.GetElementType()!)); if (actualType.IsEnum) return Enum.GetNames(actualType).FirstOrDefault(); diff --git a/PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionStrings.Designer.cs b/PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionStrings.Designer.cs new file mode 100644 index 0000000..e3ccd23 --- /dev/null +++ b/PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionStrings.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +// +// 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.UI.ClassSelection { + 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()] + public class ClassSelectionStrings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ClassSelectionStrings() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + 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.ClassSelection.ClassSelectionStrings", typeof(ClassSelectionStrings).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)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Cancel. + /// + public static string ButtonCancel { + get { + return ResourceManager.GetString("ButtonCancel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select. + /// + public static string ButtonSelect { + get { + return ResourceManager.GetString("ButtonSelect", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select class. + /// + public static string WindowTitle { + get { + return ResourceManager.GetString("WindowTitle", resourceCulture); + } + } + } +} diff --git a/PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionStrings.resx b/PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionStrings.resx new file mode 100644 index 0000000..9c4c9a0 --- /dev/null +++ b/PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionStrings.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Cancel + + + Select + + + Select class + + \ No newline at end of file diff --git a/PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionViewModel.cs b/PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionViewModel.cs new file mode 100644 index 0000000..5cecfbb --- /dev/null +++ b/PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionViewModel.cs @@ -0,0 +1,211 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Windows.Input; +using PettingZoo.Core.Generator; +using PettingZoo.WPF.ViewModel; + +namespace PettingZoo.Tapeti.UI.ClassSelection +{ + public class ClassSelectionViewModel : BaseViewModel + { + private BaseClassTreeItem? selectedItem; + private readonly DelegateCommand selectCommand; + + public ObservableCollection Examples { get; } = new(); + + public BaseClassTreeItem? SelectedItem + { + get => selectedItem; + set + { + if (!SetField(ref selectedItem, value)) + return; + + selectCommand.RaiseCanExecuteChanged(); + } + } + + + public ICommand SelectCommand => selectCommand; + + + public event EventHandler? Select; + + + public ClassSelectionViewModel(IEnumerable examples) + { + selectCommand = new DelegateCommand(SelectExecute, SelectCanExecute); + + TreeFromExamples(examples); + } + + + private void SelectExecute() + { + if (SelectedItem is not ExampleTreeItem exampleTreeItem) + return; + + Select?.Invoke(this, exampleTreeItem.Example); + } + + + private bool SelectCanExecute() + { + return SelectedItem is ExampleTreeItem; + } + + + private void TreeFromExamples(IEnumerable examples) + { + var root = new NamespaceFolderClassTreeItem(string.Empty); + + foreach (var example in examples) + { + var folder = !string.IsNullOrEmpty(example.Namespace) + ? CreateFolder(root, example.Namespace.Split('.')) + : root; + + folder.AddChild(new ExampleTreeItem(example)); + } + + + // If the first levels only consist of one child folder, collapse them into one entry + var collapsedRoot = root; + while (collapsedRoot.Children.Count == 1 && collapsedRoot.Children.First() is NamespaceFolderClassTreeItem newRoot) + collapsedRoot = newRoot.Collapse(collapsedRoot.Name); + + if (ReferenceEquals(collapsedRoot, root)) + { + foreach (var rootItem in root.Children) + Examples.Add(rootItem); + } + else + Examples.Add(collapsedRoot); + } + + + private static NamespaceFolderClassTreeItem CreateFolder(NamespaceFolderClassTreeItem root, IEnumerable parts) + { + var parent = root; + + foreach (var part in parts) + { + if (parent.Children.FirstOrDefault(c => c is NamespaceFolderClassTreeItem && c.Name == part) is NamespaceFolderClassTreeItem child) + { + parent = child; + continue; + } + + child = new NamespaceFolderClassTreeItem(part); + parent.AddChild(child); + + parent = child; + } + + return parent; + } + } + + + public class BaseClassTreeItem + { + private readonly SortedSet children = new(new BaseClassTreeItemComparer()); + + public string Name { get; protected set; } + public IReadOnlyCollection Children => children; + + + public BaseClassTreeItem(string name) + { + Name = name; + } + + + public void AddChild(BaseClassTreeItem item) + { + children.Add(item); + } + } + + + public class NamespaceFolderClassTreeItem : BaseClassTreeItem + { + public NamespaceFolderClassTreeItem(string name) : base(name) + { + } + + + public NamespaceFolderClassTreeItem Collapse(string parentFolderName) + { + Name = string.IsNullOrEmpty(parentFolderName) ? Name : parentFolderName + "." + Name; + return this; + } + } + + + public class ExampleTreeItem : BaseClassTreeItem + { + public IClassTypeExample Example { get; } + + + public ExampleTreeItem(IClassTypeExample example) : base(example.ClassName) + { + Example = example; + } + } + + + public class BaseClassTreeItemComparer : IComparer + { + public int Compare(BaseClassTreeItem? x, BaseClassTreeItem? y) + { + if (ReferenceEquals(x, y)) return 0; + if (y == null) return 1; + if (x == null) return -1; + + if (x.GetType() != y.GetType()) + return x is NamespaceFolderClassTreeItem ? -1 : 1; + + return string.Compare(x.Name, y.Name, StringComparison.Ordinal); + } + } + + + public class DesignTimeClassSelectionViewModel : ClassSelectionViewModel + { + public DesignTimeClassSelectionViewModel() + : base(new IClassTypeExample[] + { + new DesignTimeExample("Messaging.Test", "Messaging.Test", "TestMessage"), + new DesignTimeExample("Messaging.Test", "Messaging.Test", "SomeRequestMessage"), + new DesignTimeExample("Messaging.Test", "Messaging.Test.Model", "SomeViewModel") + }) + { + + } + + + private class DesignTimeExample : IClassTypeExample + { + public string AssemblyName { get; } + public string? Namespace { get; } + public string ClassName { get; } + + + public DesignTimeExample(string assemblyName, string? ns, string className) + { + AssemblyName = assemblyName; + Namespace = ns; + ClassName = className; + } + + + public string Generate() + { + return ""; + } + } + } +} diff --git a/PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionWindow.xaml b/PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionWindow.xaml new file mode 100644 index 0000000..c692ef6 --- /dev/null +++ b/PettingZoo.Tapeti/UI/ClassSelection/ClassSelectionWindow.xaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +