2021-12-31 17:48:04 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2022-01-03 14:00:30 +00:00
|
|
|
|
using System.ComponentModel.DataAnnotations;
|
2021-12-31 17:48:04 +00:00
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
2022-01-01 20:45:15 +00:00
|
|
|
|
using System.Runtime.Loader;
|
2021-12-31 17:48:04 +00:00
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
using PettingZoo.Core.Generator;
|
2022-01-03 14:00:30 +00:00
|
|
|
|
using PettingZoo.Core.Validation;
|
2022-01-14 08:13:55 +00:00
|
|
|
|
using Tapeti.Default;
|
2021-12-31 17:48:04 +00:00
|
|
|
|
|
|
|
|
|
namespace PettingZoo.Tapeti.AssemblyParser
|
|
|
|
|
{
|
|
|
|
|
public class AssemblyParser : IDisposable
|
|
|
|
|
{
|
2022-01-03 14:00:30 +00:00
|
|
|
|
private readonly string[] extraAssembliesPaths;
|
|
|
|
|
private AssemblyLoadContext? loadContext;
|
|
|
|
|
|
2021-12-31 17:48:04 +00:00
|
|
|
|
|
2022-01-01 20:45:15 +00:00
|
|
|
|
public AssemblyParser(params string[] extraAssembliesPaths)
|
2021-12-31 17:48:04 +00:00
|
|
|
|
{
|
2022-01-03 14:00:30 +00:00
|
|
|
|
this.extraAssembliesPaths = extraAssembliesPaths;
|
2021-12-31 17:48:04 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
2022-01-03 14:00:30 +00:00
|
|
|
|
loadContext?.Unload();
|
2021-12-31 17:48:04 +00:00
|
|
|
|
GC.SuppressFinalize(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public IEnumerable<IClassTypeExample> GetExamples(Stream assemblyStream)
|
|
|
|
|
{
|
2022-01-03 14:00:30 +00:00
|
|
|
|
if (loadContext == null)
|
|
|
|
|
{
|
|
|
|
|
/*
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
foreach (var extraAssembly in extraAssembliesPaths.SelectMany(p => Directory.Exists(p)
|
|
|
|
|
? Directory.GetFiles(p, "*.dll")
|
|
|
|
|
: Enumerable.Empty<string>()))
|
|
|
|
|
{
|
|
|
|
|
loadContext.LoadFromAssemblyPath(extraAssembly);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-31 17:48:04 +00:00
|
|
|
|
var assembly = loadContext.LoadFromStream(assemblyStream);
|
|
|
|
|
|
|
|
|
|
foreach (var type in assembly.GetTypes().Where(t => t.IsClass))
|
|
|
|
|
yield return new TypeExample(type);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2022-01-03 14:00:30 +00:00
|
|
|
|
private class TypeExample : IClassTypeExample, IValidatingExample
|
2021-12-31 17:48:04 +00:00
|
|
|
|
{
|
|
|
|
|
public string AssemblyName => type.Assembly.GetName().Name ?? "";
|
|
|
|
|
public string? Namespace => type.Namespace;
|
|
|
|
|
public string ClassName => type.Name;
|
|
|
|
|
|
|
|
|
|
private readonly Type type;
|
|
|
|
|
|
2022-01-03 14:00:30 +00:00
|
|
|
|
private bool validationInitialized;
|
|
|
|
|
private bool validationAvailable;
|
|
|
|
|
|
2021-12-31 17:48:04 +00:00
|
|
|
|
|
|
|
|
|
public TypeExample(Type type)
|
|
|
|
|
{
|
|
|
|
|
this.type = type;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public string Generate()
|
|
|
|
|
{
|
|
|
|
|
var serialized = TypeToJObjectConverter.Convert(type);
|
|
|
|
|
return serialized.ToString(Formatting.Indented);
|
|
|
|
|
}
|
2022-01-03 14:00:30 +00:00
|
|
|
|
|
|
|
|
|
|
2022-01-14 08:13:55 +00:00
|
|
|
|
public bool TryGetPublishDestination(out string exchange, out string routingKey)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// Assume default strategies are used
|
|
|
|
|
exchange = new NamespaceMatchExchangeStrategy().GetExchange(type);
|
|
|
|
|
routingKey = new TypeNameRoutingKeyStrategy().GetRoutingKey(type);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
exchange = "";
|
|
|
|
|
routingKey = "";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2022-01-03 14:00:30 +00:00
|
|
|
|
public bool CanValidate()
|
|
|
|
|
{
|
|
|
|
|
return InitializeValidation();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public void Validate(string payload)
|
|
|
|
|
{
|
|
|
|
|
if (!InitializeValidation())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// Json exceptions are already handled by the PayloadEditorViewModel
|
|
|
|
|
var deserialized = JsonConvert.DeserializeObject(payload, type);
|
|
|
|
|
if (deserialized == null)
|
|
|
|
|
throw new PayloadValidationException(AssemblyParserStrings.JsonDeserializationNull, null);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var validationContext = new ValidationContext(deserialized);
|
|
|
|
|
Validator.ValidateObject(deserialized, validationContext, true);
|
|
|
|
|
}
|
|
|
|
|
catch (ValidationException e)
|
|
|
|
|
{
|
|
|
|
|
var members = string.Join(", ", e.ValidationResult.MemberNames);
|
|
|
|
|
if (!string.IsNullOrEmpty(members))
|
|
|
|
|
throw new PayloadValidationException(string.Format(AssemblyParserStrings.ValidationErrorsMembers, members, e.ValidationResult.ErrorMessage), null);
|
|
|
|
|
|
|
|
|
|
throw new PayloadValidationException(string.Format(AssemblyParserStrings.ValidationErrors, e.ValidationResult.ErrorMessage), null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private bool InitializeValidation()
|
|
|
|
|
{
|
|
|
|
|
if (validationInitialized)
|
|
|
|
|
return validationAvailable;
|
|
|
|
|
|
|
|
|
|
// Attempt to create an instance (only works if all dependencies are present, which is not yet
|
|
|
|
|
// guaranteed because we aren't fetching NuGet dependencies yet). We're giving it a fighting chance
|
|
|
|
|
// by referencing Tapeti.Annotations, System.ComponentModel.Annotations and Tapeti.DataAnnotations.Extensions in
|
|
|
|
|
// this class library.
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var instance = Activator.CreateInstance(type);
|
|
|
|
|
if (instance != null)
|
|
|
|
|
{
|
|
|
|
|
// Attributes are only evaluated when requested, so call validation once to give it a better chance to
|
|
|
|
|
// detect if we'll be able to validate the message
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var validationContext = new ValidationContext(instance);
|
|
|
|
|
Validator.ValidateObject(instance, validationContext, true);
|
|
|
|
|
|
|
|
|
|
validationAvailable = true;
|
|
|
|
|
}
|
|
|
|
|
catch (ValidationException)
|
|
|
|
|
{
|
|
|
|
|
// The fact that it validated is good enough, this can be expected with an empty object
|
|
|
|
|
validationAvailable = true;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception)
|
|
|
|
|
{
|
|
|
|
|
// ignored
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception)
|
|
|
|
|
{
|
|
|
|
|
// No go, try to create an example without validation
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
validationInitialized = true;
|
|
|
|
|
return validationAvailable;
|
|
|
|
|
}
|
2021-12-31 17:48:04 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|