diff --git a/PettingZoo.Core/Generator/IExampleGenerator.cs b/PettingZoo.Core/Generator/IExampleGenerator.cs
index 95b0e95..f8fdf44 100644
--- a/PettingZoo.Core/Generator/IExampleGenerator.cs
+++ b/PettingZoo.Core/Generator/IExampleGenerator.cs
@@ -1,4 +1,5 @@
using System;
+using PettingZoo.Core.Validation;
namespace PettingZoo.Core.Generator
{
@@ -20,14 +21,11 @@ namespace PettingZoo.Core.Generator
public string? Namespace { get; }
public string ClassName { get; }
- public string FullClassName => !string.IsNullOrEmpty(Namespace) ? Namespace + "." : "" + ClassName;
+ public string FullClassName => (!string.IsNullOrEmpty(Namespace) ? Namespace + "." : "") + ClassName;
}
- /*
- public interface IValidatingExample : IExample
+ public interface IValidatingExample : IExample, IPayloadValidator
{
- bool Validate(string payload, out string validationMessage);
}
- */
}
diff --git a/PettingZoo.Core/Rendering/MessageBodyRenderer.cs b/PettingZoo.Core/Rendering/MessageBodyRenderer.cs
index 029be9c..4482da1 100644
--- a/PettingZoo.Core/Rendering/MessageBodyRenderer.cs
+++ b/PettingZoo.Core/Rendering/MessageBodyRenderer.cs
@@ -18,8 +18,6 @@ namespace PettingZoo.Core.Rendering
return contentType != null && ContentTypeHandlers.TryGetValue(contentType, out var handler)
? handler(body)
: Encoding.UTF8.GetString(body);
-
- // ToDo hex output if required
}
diff --git a/PettingZoo.Core/Validation/IPayloadValidator.cs b/PettingZoo.Core/Validation/IPayloadValidator.cs
new file mode 100644
index 0000000..8eb9dcf
--- /dev/null
+++ b/PettingZoo.Core/Validation/IPayloadValidator.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace PettingZoo.Core.Validation
+{
+ public class PayloadValidationException : Exception
+ {
+ public TextPosition? ErrorPosition { get; }
+
+
+ public PayloadValidationException(string message, TextPosition? errorPosition) : base(message)
+ {
+ ErrorPosition = errorPosition;
+ }
+ }
+
+
+
+ public interface IPayloadValidator
+ {
+ bool CanValidate();
+
+ ///
+ void Validate(string payload);
+ }
+}
diff --git a/PettingZoo/UI/TextPosition.cs b/PettingZoo.Core/Validation/TextPosition.cs
similarity index 96%
rename from PettingZoo/UI/TextPosition.cs
rename to PettingZoo.Core/Validation/TextPosition.cs
index ed679eb..a024954 100644
--- a/PettingZoo/UI/TextPosition.cs
+++ b/PettingZoo.Core/Validation/TextPosition.cs
@@ -1,6 +1,6 @@
using System;
-namespace PettingZoo.UI
+namespace PettingZoo.Core.Validation
{
public readonly struct TextPosition : IEquatable
{
diff --git a/PettingZoo.Tapeti/AssemblyParser/AssemblyParser.cs b/PettingZoo.Tapeti/AssemblyParser/AssemblyParser.cs
index b7a5072..50c5168 100644
--- a/PettingZoo.Tapeti/AssemblyParser/AssemblyParser.cs
+++ b/PettingZoo.Tapeti/AssemblyParser/AssemblyParser.cs
@@ -1,44 +1,55 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Runtime.Loader;
using Newtonsoft.Json;
using PettingZoo.Core.Generator;
+using PettingZoo.Core.Validation;
namespace PettingZoo.Tapeti.AssemblyParser
{
public class AssemblyParser : IDisposable
{
- private readonly AssemblyLoadContext loadContext;
+ private readonly string[] extraAssembliesPaths;
+ private AssemblyLoadContext? loadContext;
+
public AssemblyParser(params string[] extraAssembliesPaths)
{
- // 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()))
- {
- loadContext.LoadFromAssemblyPath(extraAssembly);
- }
+ this.extraAssembliesPaths = extraAssembliesPaths;
}
public void Dispose()
{
- loadContext.Unload();
+ loadContext?.Unload();
GC.SuppressFinalize(this);
}
public IEnumerable GetExamples(Stream assemblyStream)
{
+ 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()))
+ {
+ loadContext.LoadFromAssemblyPath(extraAssembly);
+ }
+ }
+
var assembly = loadContext.LoadFromStream(assemblyStream);
foreach (var type in assembly.GetTypes().Where(t => t.IsClass))
@@ -47,7 +58,7 @@ namespace PettingZoo.Tapeti.AssemblyParser
- private class TypeExample : IClassTypeExample
+ private class TypeExample : IClassTypeExample, IValidatingExample
{
public string AssemblyName => type.Assembly.GetName().Name ?? "";
public string? Namespace => type.Namespace;
@@ -55,6 +66,9 @@ namespace PettingZoo.Tapeti.AssemblyParser
private readonly Type type;
+ private bool validationInitialized;
+ private bool validationAvailable;
+
public TypeExample(Type type)
{
@@ -64,20 +78,85 @@ namespace PettingZoo.Tapeti.AssemblyParser
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);
}
+
+
+ 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;
+ }
}
}
}
diff --git a/PettingZoo.Tapeti/AssemblyParser/AssemblyParserStrings.Designer.cs b/PettingZoo.Tapeti/AssemblyParser/AssemblyParserStrings.Designer.cs
new file mode 100644
index 0000000..56d9f10
--- /dev/null
+++ b/PettingZoo.Tapeti/AssemblyParser/AssemblyParserStrings.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.AssemblyParser {
+ 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 AssemblyParserStrings {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal AssemblyParserStrings() {
+ }
+
+ ///
+ /// 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.AssemblyParser.AssemblyParserStrings", typeof(AssemblyParserStrings).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 JSON deserialization returned null.
+ ///
+ internal static string JsonDeserializationNull {
+ get {
+ return ResourceManager.GetString("JsonDeserializationNull", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Failed validation: {0}.
+ ///
+ internal static string ValidationErrors {
+ get {
+ return ResourceManager.GetString("ValidationErrors", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Failed validation for {0}: {1}.
+ ///
+ internal static string ValidationErrorsMembers {
+ get {
+ return ResourceManager.GetString("ValidationErrorsMembers", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/PettingZoo.Tapeti/AssemblyParser/AssemblyParserStrings.resx b/PettingZoo.Tapeti/AssemblyParser/AssemblyParserStrings.resx
new file mode 100644
index 0000000..2ed858f
--- /dev/null
+++ b/PettingZoo.Tapeti/AssemblyParser/AssemblyParserStrings.resx
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+ JSON deserialization returned null
+
+
+ Failed validation: {0}
+
+
+ Failed validation for {0}: {1}
+
+
\ No newline at end of file
diff --git a/PettingZoo.Tapeti/NuGet/NuGetPackageManager.cs b/PettingZoo.Tapeti/NuGet/NuGetPackageManager.cs
index def5e42..6c66477 100644
--- a/PettingZoo.Tapeti/NuGet/NuGetPackageManager.cs
+++ b/PettingZoo.Tapeti/NuGet/NuGetPackageManager.cs
@@ -27,6 +27,7 @@ namespace PettingZoo.Tapeti.NuGet
public NuGetPackageManager(ILogger logger)
{
this.logger = logger;
+
cache = new SourceCacheContext();
sources = new List