Fixed example generation

Additional logging
This commit is contained in:
Mark van Renswoude 2022-01-01 21:45:15 +01:00
parent db443556e8
commit 8ca67c2cc5
18 changed files with 231 additions and 76 deletions

View File

@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Serilog" Version="2.10.0" />
</ItemGroup>
<ItemGroup>

View File

@ -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()
{

View File

@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="RabbitMQ.Client" Version="6.2.2" />
<PackageReference Include="Serilog" Version="2.10.0" />
</ItemGroup>
<ItemGroup>

View File

@ -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");
}

View File

@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="LiteDB" Version="5.0.11" />
<PackageReference Include="LiteDB.Async" Version="0.0.11" />
<PackageReference Include="Serilog" Version="2.10.0" />
</ItemGroup>
<ItemGroup>

View File

@ -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<string>()))
{
loadContext.LoadFromAssemblyPath(extraAssembly);
}
}
public void Dispose()
{
loadContext.Dispose();
loadContext.Unload();
GC.SuppressFinalize(this);
}

View File

@ -6,7 +6,6 @@ using System.Threading.Tasks;
namespace PettingZoo.Tapeti.NuGet
{
// TODO support logger
public interface INuGetPackageManager
{
public IReadOnlyList<INuGetPackageSource> 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);
}
}

View File

@ -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<Source> sources;
public IReadOnlyList<INuGetPackageSource> Sources => sources;
public NuGetPackageManager()
public NuGetPackageManager(ILogger logger)
{
this.logger = logger;
cache = new SourceCacheContext();
sources = new List<Source>
{
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<INuGetPackage>();
var resource = await repository.GetResourceAsync<PackageSearchResource>(cancellationToken);
var filter = new SearchFilter(includePrerelease);
try
{
var resource = await repository.GetResourceAsync<PackageSearchResource>(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<INuGetPackageVersion>? 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<IReadOnlyList<INuGetPackageVersion>> 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;
}
}
}

View File

@ -11,6 +11,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NuGet.Packaging" Version="6.0.0" />
<PackageReference Include="NuGet.Protocol" Version="6.0.0" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="SharpVectors" Version="1.7.7" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Reactive" Version="5.0.0" />

View File

@ -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<IExample> 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<IClassTypeExample> LoadExamples(IEnumerable<IPackageAssembly> 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 =>
{

View File

@ -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<Type>());
}
private static readonly Dictionary<Type, Type> 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<Type, JToken> TypeValueMap = new()
{
{ typeof(int), 0 },
{ typeof(decimal), 0.0 },
{ typeof(bool), false }
};
private static JObject ClassToJToken(Type classType, IEnumerable<Type> 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<Type, JToken> TypeMap = new()
private static JToken TypeToJToken(Type type, ICollection<Type> 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})";
}
}
}

View File

@ -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

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="FluentAssertions.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PettingZoo.Tapeti\PettingZoo.Tapeti.csproj" />
</ItemGroup>
</Project>

View File

@ -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<string>();
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
}
}

View File

@ -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

View File

@ -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()

View File

@ -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
- Validation against message classes (for Tapeti messages)

View File

@ -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
});
});
}