Implementation for the Open API validation#152
Conversation
|
|
||
| OpenApiExtensibleRules.ExtensionNameMustStartWithXDash, | ||
| // add more default rules. | ||
| }; |
There was a problem hiding this comment.
This will be difficult to track. People will forget to put the new rules in here since this list is in a different file than the rules themselves.
I am thinking of this:
- Add DefaultRuleSet for every OpenApiXXXRules file
- This main DefaultRuleSet is a concatenation of all DefaultRuleSet's
What do you think? #Closed
There was a problem hiding this comment.
In fact, we might not need this concatenated list at all since it will be broken down in Add() below anyway.
In reply to: 157320269 [](ancestors = 157320269)
There was a problem hiding this comment.
-
Basically, I want to use the attribute for each "DOM" classes same as Add the validator attribute, validation rule and validator #133.
Let me think it more. -
Why do you think it will break the Add() method? That's in deed will call "Add()" method. #Resolved
There was a problem hiding this comment.
If you wanted to create an OpenApiValidationRuleAttribute and annotate each rule with that attribute and then have a discovery mechanism that searched the assembly to find all classes with that attribute, then I would have no objection to that.
In reply to: 157321750 [](ancestors = 157321750)
There was a problem hiding this comment.
Overall I like the direction of this PR. I think by adding a simple marker attribute to each rule, you can reflect over the rules and built a default ruleset. You should be able to determine the target type from the generic return type of the rule.
This should address the concern brought up by @PerthCharern that someone might forget to add the rule to the ruleset. #Resolved
There was a problem hiding this comment.
@xuzhg I didn't mean it will break the Add() method. I meant the list didn't have to be created because it would be enumerated in the Add() method anyway, so we can find a way to make it less complicated.
Anyway, I like @darrelmiller 's suggestion here. That would put the concern of when each rule should be used back to the rule itself. #Closed
There was a problem hiding this comment.
There was a problem hiding this comment.
There was a problem hiding this comment.
|
Can I make a suggestion for doing these kind of design reviews? It would be helpful to see the code changes for a couple of examples rather than a complete implementation. Trying to review changes to 60 files in the GitHub UI is harder than it should be. #WontFix |
|
|
||
| /// <summary> | ||
| /// Validate the Open API element. | ||
| /// </summary> |
There was a problem hiding this comment.
Ok, so here's the deal! If I have to put up with these horrible comment blocks, I would like to see comments that actually justify their existence. So let's make the comment explain why I would pick one overload over the other so that I don't have to guess the reason based on what parameters are there. #Resolved
There was a problem hiding this comment.
There was a problem hiding this comment.
| public ValidationContext(ValidationRuleSet ruleSet) | ||
| { | ||
| RuleSet = ruleSet ?? throw Error.ArgumentNull(nameof(ruleSet)); | ||
| } |
There was a problem hiding this comment.
This is a personal preference, but I really don't like this syntax. I'd much rather see a guard clause and a separate assignment. Could we even use the Code Contracts library that would do static analysis? #WontFix
There was a problem hiding this comment.
Will we have to enforce this though? It's a valid and acceptable C# syntax as far as I know, and I'm not sure if we should enforce it one way or another when someone submits a PR.
In reply to: 157325008 [](ancestors = 157325008)
There was a problem hiding this comment.
There was a problem hiding this comment.
It's also VS recommendation to sort System. namespaces after Microsoft. namespaces. There be dragons with that argument ;-)
There was a problem hiding this comment.
And nobody likes the idea of using Code Contracts so we actually get static analysis for these issues too?
| /// The base visitor class for Open Api elements. | ||
| /// </summary> | ||
| internal abstract class VisitorBase<T> : IVisitor where T : IOpenApiElement | ||
| { |
There was a problem hiding this comment.
This class has no state and we have an IVisitor interface. Is there any reason these two methods couldn't just be library functions? I don't think we need a base class for all the visitors. #Pending
There was a problem hiding this comment.
Each visitor has two parts:
- Visit itself's properties
- Go to visit it's Children.
Where:
a. Visit() method is for the #1, it has the same logic for all elements. So, put its at the base class.
b. Next() method is for the #2, it has different logic for all elements. So, each element visitor class should override Next() method to list the children.
In reply to: 157326815 [](ancestors = 157326815)
There was a problem hiding this comment.
Part of the reason I initially used the visitor pattern was so that we could define the traversal of the OpenAPI DOM just once and then have different visitors for different purposes. Validation is the obvious Visitor, but code generation could be another. Either client code gen, or server code scaffolding.
The Visitor related classes in this PR are specifically tied to validation via the ValidationContext. Even the abstract base class and IVisitor interface.
If you look at the Visitor pattern https://en.wikipedia.org/wiki/Visitor_pattern here the Visitor implements all the visit methods for all the elements it is interested in. This allows the Visitor to hold shared state whilst doing the traversal. It also plays the role of the context object.
With the current approach, ignoring the fact that it is tied to validation, each new "visiting" capability needs to create classes for each and every element and then pass the context between them.
I'm going to see if I can use my walker and validationVisitor to execute your rules classes and we can compare the end result. #Pending
There was a problem hiding this comment.
There was a problem hiding this comment.
I was able to incorporate your Rules classes into the OpenApiValidator like this,
public class OpenApiValidator : OpenApiVisitorBase
{
readonly ValidationRuleSet _ruleSet;
readonly ValidationContext _context;
/// <summary>
/// Create a vistor that will validate an OpenAPIDocument
/// </summary>
/// <param name="ruleSet"></param>
public OpenApiValidator(ValidationRuleSet ruleSet = null)
{
_ruleSet = ruleSet ?? ValidationRuleSet.DefaultRuleSet;
_context = new ValidationContext(_ruleSet);
}
public override void Visit(OpenApiDocument item) => Validate(item);
public override void Visit(OpenApiInfo item) => Validate(item);
public override void Visit(OpenApiContact item) => Validate(item);
public override void Visit(OpenApiResponse item) => Validate(item);
public IEnumerable<ValidationError> Errors => _context.Errors;
private void Validate<T>(T item)
{
if (item == null) return; // Required fields should be checked by higher level objects
var rules = _ruleSet.Where(r => r.ElementType == typeof(T));
foreach (var rule in rules)
{
rule.Evaluate(_context, item);
}
}
}
}Obviously this doesn't have the Visit methods for all types yet, and some types may be more complex than just the simple expression. Also, the walker class would need to build the property path.
However, using this approach, we need one class for walking the DOM (we could create other walkers if other visitors need to traverse in a different way) and one class for dispatching the Visit methods to the rules. This makes implementing other types of Visitors easier by only requiring one new class.
My understanding is that this approach would remove the need for :
- the Visitor class for each element
- the OpenApiVisitorSet
- the ValidationContextExtensions
I'm still not quite sure how the ValidationMap and ValidateCollection extensions fit into this approach.
#Pending
There was a problem hiding this comment.
It's ok. Please share your thoughts and codes. We can discuss to select one or just merge together.
In reply to: 157841020 [](ancestors = 157841020)
|
@darrelmiller @PerthCharern I added the attribute for Rule classes. Would you please take a look at da5f90a #WontFix |
|
@darrelmiller @PerthCharern I also added the attribute for visitor classes. Would you please take a look at : 4a6ca43 #WontFix |
| { | ||
| if (_visitors == null) | ||
| { | ||
| _visitors = new Lazy<IDictionary<Type, IVisitor>>(() => BuildVisitorSet(), isThreadSafe: false).Value; |
There was a problem hiding this comment.
Maybe Lazy is unnecessary? #Closed
There was a problem hiding this comment.
I would say the underlying operation is not that time consuming/complex, so I think it's unnecessary. But I don't really mind using it if you prefer.
In reply to: 157624690 [](ancestors = 157624690)
There was a problem hiding this comment.
| /// <summary> | ||
| /// The Validator attribute. | ||
| /// </summary> | ||
| [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] |
There was a problem hiding this comment.
Class [](start = 37, length = 5)
Why class attribute vs field/property attribute? #Resolved
There was a problem hiding this comment.
It's easy to target "Rule" Class.
If it's target on the Rule property, we should go through all the properties in all the types, it's time consuming. #Pending
|
You just meant the logic to traverse all the Visitors, not a C# attribute, right? I want to make sure I'm not missing something. In reply to: 352585589 [](ancestors = 352585589) |
|
@PerthCharern For the visitor, Yes. I have the interface |
|
|
||
| // Assert | ||
| var exception = Assert.Throws<OpenApiException>(test); | ||
| Assert.Equal("Can not find visitor type registered for type 'Microsoft.OpenApi.Validations.Visitors.Tests.OpenApiVisitorSetTests'.", |
There was a problem hiding this comment.
Can not find visitor type registered for type [](start = 26, length = 45)
This should be a public const in the main project and referenced here. #Pending
There was a problem hiding this comment.
Yes. I can use the
Assert.Equal(String.Format(SRResource.xxxxx, typeof(xxx).FullName), exception.Message);
However, it looks not good than the plain text in the test project. Because it can help us verify the error message thrown as our expected. What do you think?
#WontFix
There was a problem hiding this comment.
Yes. I can use the
Assert.Equal(String.Format(SRResource.xxxxx, typeof(xxx).FullName), exception.Message);
However, it looks not good than the plain text in the test project. Because it can help us verify the error message thrown as our expected. What do you think?
In reply to: 157649686 [](ancestors = 157649686)
There was a problem hiding this comment.
I think we should go with that instead of duplicating string literals. We are essentially checking that the exception thrown has the "right string", not that it is spelled correctly, etc.
I would say we get better verification from referring to the constant string in SRResource.
In reply to: 157832135 [](ancestors = 157832135)
There was a problem hiding this comment.
| public void VisitorsPropertyReturnsTheCorrectVisitorList() | ||
| { | ||
| for (int i = 0; i < 5; i++) // 5 is just a magic number | ||
| { |
There was a problem hiding this comment.
Not sure what the loop helps with. Maybe remove? #Closed
There was a problem hiding this comment.
I just want to test multiple retrieving the property "Visitors" can work as expected. #Closed
There was a problem hiding this comment.
I just want to test multiple retrieving the property "Visitors" can work as expected.
In reply to: 157649800 [](ancestors = 157649800)
| // Assert | ||
| Assert.NotNull(visitors); | ||
| Assert.NotEmpty(visitors); | ||
| Assert.Equal(29, visitors.Count); |
There was a problem hiding this comment.
Assert.Equal(29, visitors.Count); [](start = 16, length = 33)
Provide comment #Closed
| [InlineData(typeof(OpenApiDocument), typeof(DocumentVisitor))] | ||
| [InlineData(typeof(OpenApiInfo), typeof(InfoVisitor))] | ||
| [InlineData(typeof(OpenApiXml), typeof(XmlVisitor))] | ||
| [InlineData(typeof(OpenApiComponents), typeof(ComponentsVisitor))] |
There was a problem hiding this comment.
[InlineData(typeof(OpenApiComponents), typeof(ComponentsVisitor))] [](start = 8, length = 66)
Can we add all types here? It's a one time thing, and it will help ensure no mistakes in the real code. #Closed
There was a problem hiding this comment.
| ruleSet.Add(rule); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
It's a little risky to me that we rely on the type and not an attribute assigned to each property. #Closed
There was a problem hiding this comment.
It is okay for now though. I don't have a better way than to tag all properties, which is something we want to avoid.
In reply to: 157650194 [](ancestors = 157650194)
There was a problem hiding this comment.
Yes. Basically, that's convention. It's ok if we follow up this convention.
In reply to: 157650606 [](ancestors = 157650606,157650194)
| { | ||
| rule.Evaluate(context, extensbile); | ||
| } | ||
| } |
There was a problem hiding this comment.
Why is this not done in Visit above? #Pending
There was a problem hiding this comment.
:) My design is
- Visit() method is used to verify the
itselfproperities - Next() method is used to verify the
Childrenelement.
So, I think Extension belongs to the Children scope. What do you think? #WontFix
There was a problem hiding this comment.
:) My design is
- Visit() method is used to verify the itself properities
- Next() method is used to verify the Children element.
So, I think Extension belongs to the Children scope. What do you think?
In reply to: 157650373 [](ancestors = 157650373)
There was a problem hiding this comment.
Why does Extension fall into the Children scope? Isn't it one of the properties?
In reply to: 157838009 [](ancestors = 157838009,157650373)
| /// </summary> | ||
| /// <param name="context">The validation context.</param> | ||
| /// <param name="item">The element.</param> | ||
| public void Visit(ValidationContext context, object item) |
There was a problem hiding this comment.
object [](start = 53, length = 6)
Can this be T? #WontFix
There was a problem hiding this comment.
| /// <param name="element">The element.</param> | ||
| protected virtual void Next(ValidationContext context, T element) | ||
| { | ||
| IOpenApiExtensible extensbile = element as IOpenApiExtensible; |
There was a problem hiding this comment.
extensbile [](start = 31, length = 10)
typo, extensible #Closed
There was a problem hiding this comment.
Resolved. #Resolved
| Assert.NotNull(errors); | ||
|
|
||
| Assert.Equal(new[] { "The field 'url' in 'info' object is REQUIRED.", | ||
| "The field 'version' in 'info' object is REQUIRED."}, errors.Select(e => e.ErrorMessage)); |
There was a problem hiding this comment.
These strings in all test classes should come from the main project, so that we don't need to keep track of the strings in two places. #Pending
There was a problem hiding this comment.
Same as others:
Yes. I can use the
Assert.Equal(String.Format(SRResource.xxxxx, typeof(xxx).FullName), exception.Message);
However, it looks not good than the plain text in the test project. Because it can help us verify the error message thrown as expected. What do you think?
In reply to: 157650687 [](ancestors = 157650687)
| /// <returns>The error string.</returns> | ||
| public override string ToString() | ||
| { | ||
| return "ErroCode: " + ErrorCode + ", " + ErrorPath + " | " + ErrorMessage; |
There was a problem hiding this comment.
ErroCode [](start = 20, length = 8)
typo, ErrorCode #Closed
| Assert.False(result); | ||
| Assert.NotNull(errors); | ||
| ValidationError error = Assert.Single(errors); | ||
| Assert.Equal("The field 'description' in 'response' object is REQUIRED.", error.ErrorMessage); |
There was a problem hiding this comment.
Should we check that the path and the reason are also as expected? #Closed
There was a problem hiding this comment.
Good catch. I added such here.
Such test cases is helping us to understand the design. I'd like to make sure we can finalize the design first.
In reply to: 157651079 [](ancestors = 157651079)
| protected override void Next(ValidationContext context, OpenApiPathItem pathItem) | ||
| { | ||
| Debug.Assert(context != null); | ||
| Debug.Assert(pathItem != null); |
There was a problem hiding this comment.
We haven't been using this anywhere else. Can we be consistent and use the normal null checks? #Pending
There was a problem hiding this comment.
There was a problem hiding this comment.
protected class is practically a public class (since our customer can technically just create a class that inherits the PathItemVisitor and calls the method).
I think we should do the null check here.
In reply to: 157838274 [](ancestors = 157838274,157651170)
| new ValidationRule<OpenApiExternalDocs>( | ||
| (context, item) => | ||
| { | ||
| // title |
There was a problem hiding this comment.
title [](start = 23, length = 5)
url #Closed
There was a problem hiding this comment.
| new ValidationRule<OpenApiResponse>( | ||
| (context, response) => | ||
| { | ||
| // title |
There was a problem hiding this comment.
title [](start = 23, length = 5)
description #Resolved
| public static class OpenApiTagRules | ||
| { | ||
| /// <summary> | ||
| /// REQUIRED. |
There was a problem hiding this comment.
REQUIRED [](start = 12, length = 8)
change comment #Resolved
| /// Input string must be in the format of an email address | ||
| /// </summary> | ||
| /// <param name="input">The input string.</param> | ||
| /// <returns></returns> |
There was a problem hiding this comment.
remove or fill in #Resolved
| /// </summary> | ||
| /// <param name="input">The input string.</param> | ||
| /// <returns></returns> | ||
| public static bool IsEmailAddress(this string input) |
There was a problem hiding this comment.
IsEmailAddress [](start = 27, length = 14)
The accepted answer here looks reasonable to me:
https://stackoverflow.com/questions/1365407/c-sharp-code-to-validate-email-address
It is though a little more forgiving than it has to be, but I believe it is strict enough for our purpose. #WontFix
|
For the visitor, Yes. I have the interface IVisitor. That's enough. In reply to: 352597979 [](ancestors = 352597979,352585589) |
| /// <param name="item">The element.</param> | ||
| public void Visit(ValidationContext context, object item) | ||
| { | ||
| Debug.Assert(item is T, "item should be " + typeof(T)); |
There was a problem hiding this comment.
Debug.Assert(item is T, "item should be " + typeof(T)); [](start = 12, length = 55)
Remove Debug.Assert and use a normal check #Resolved
…the product codes
See #125:
I want to implement the validation logic for the Open API element. Here's my design (it's not finished).
General design:
Can validation each part individually. for example, Developer create a object, he can call the validate api to validate the created object.
Validate rule can be customized. For example, developer can create their own validation rules.
Detail:
OpenApiElementValidator.cs has the public apis
.\Validations\Visitors has the visitor classes for each element.
.\Validations\Rules has the rule classes for each element.
Please have a look and give me some feedback.