Validation for custom extensions#176
Conversation
| private OpenApiReaderSettings _settings; | ||
|
|
||
| /// <summary> | ||
| /// |
| /// | ||
| /// </summary> | ||
| /// <param name="settings"></param> | ||
| public OpenApiStreamReader(OpenApiReaderSettings settings = null) |
There was a problem hiding this comment.
it's better to separate two constructors? #WontFix
There was a problem hiding this comment.
I don't think so. The optional parameter conveys that settings are optional and having one constructor prevents someone from adding code to one ctor that needs to be called from both.
In reply to: 161353699 [](ancestors = 161353699)
| public AnyType AnyType { get; } = AnyType.Object; | ||
|
|
||
| /// <summary> | ||
| /// |
| /// Write out contents of OpenApiArray to passed writer | ||
| /// </summary> | ||
| /// <param name="writer">Instance of JSON or YAML writer.</param> | ||
| public void Write(IOpenApiWriter writer) |
There was a problem hiding this comment.
public? not internal? #WontFix
There was a problem hiding this comment.
Yes it needs to be public because it is part of an interface that will be implemented by any custom extension implementation.
In reply to: 161353844 [](ancestors = 161353844)
| { | ||
| readonly ValidationRuleSet _ruleSet; | ||
| readonly ValidationContext _context; | ||
|
|
| private readonly Stack<string> _path = new Stack<string>(); | ||
|
|
||
| /// <summary> | ||
| /// Allow Rule to indicate validation error occured a deeper context level. |
There was a problem hiding this comment.
[](start = 59, length = 1)
nit, preposition missing
There was a problem hiding this comment.
| foreach (var pathItem in paths) | ||
| { | ||
| Walk(pathItem); | ||
| _visitor.Enter(pathItem.Key.Replace("/","~1")); |
There was a problem hiding this comment.
~1" [](start = 57, length = 3)
nit, could you put comment in the code to explain that "~1" is an escape character for "/" in JSON pointer? #Closed
| @@ -101,6 +115,7 @@ internal void Walk(IList<OpenApiServer> servers) | |||
| Walk(server); | |||
| } | |||
There was a problem hiding this comment.
We should add the array index to get the correct json pointer here.
for ( int i =0; i<servers.Count(); i++ )
{
_visitor.Enter(i);
Walk(servers[i]);
_visitor.Exit();
} #Closed
| foreach (var tag in tags) | ||
| { | ||
| Walk(tag); | ||
| } |
There was a problem hiding this comment.
We should add the array index to get the correct json pointer here.
for ( int i =0; i<tags.Count(); i++ )
{
_visitor.Enter(i);
Walk(tags[i]);
_visitor.Exit();
}
Also, this raises another point that the Enter call should be the responsibility of the caller. In this case, the Walk(tags[i]) should NOT need to call any enter for the Tag object itself (since the array index is used instead). #Closed
| /// <param name="tags"></param> | ||
| internal void Walk(IList<OpenApiTag> tags) | ||
| { | ||
| _visitor.Enter(OpenApiConstants.Tags); |
There was a problem hiding this comment.
_visitor.Enter(OpenApiConstants.Tags); [](start = 8, length = 42)
Enter/Exit should be responsibility of the caller (e.g. in this case, it should go in the Walk(doc) ).
This will ensure that we don't do duplicate Enter calls for an object in an array (like the tags array). #Closed
There was a problem hiding this comment.
You already kinda handled this for Tag (but not for Server), but moving the Error/Exits to the caller altogether would make it more consistent.
In reply to: 162477655 [](ancestors = 162477655)
There was a problem hiding this comment.
I opened another comment to discuss this.
In reply to: 162478481 [](ancestors = 162478481,162477655)
| foreach (var parameter in parameters) | ||
| { | ||
| Walk(parameter); | ||
| } |
There was a problem hiding this comment.
need indexing - see my other comments. #Closed
| return _errors; | ||
| } | ||
| } | ||
| void Exit(); |
There was a problem hiding this comment.
Why are these needed in the context? #Closed
There was a problem hiding this comment.
Because often the rules validate additional depth. Rules add segments for properties in objects being validated. Also, for custom validation rules may validate several levels of object depth.
In reply to: 162479370 [](ancestors = 162479370)
| /// Class containing dispatchers to execute validation rules on for Open API document. | ||
| /// </summary> | ||
| public class OpenApiValidator : OpenApiVisitorBase | ||
| public class OpenApiValidator : OpenApiVisitorBase, IValidationContext |
There was a problem hiding this comment.
, IValidationContext [](start = 54, length = 21)
It's weird that the Validator is implementing the Context interface. Do we need the context interface at all?
There was a problem hiding this comment.
We could pass the entire validator object into rules, however this exposes a bunch of properties that Rules don't need access to. The way it was implemented before was the state of the validator was refactored out into an aggregated object and the state object was passed into the Rules. Rather than have the validator be made up of two objects, I just use the IValidationContext as a filter to limit what parts of the Validator the rules are allowed to access.
In reply to: 162479509 [](ancestors = 162479509)
There was a problem hiding this comment.
Ah that makes sense. Thanks for the explanation.
I think the design itself is fine, but the nomenclature still throws me off a bit. I would expect the interface to be called something along the line of "IValidationContextHolder".
That name is mouthful though... so I'm also fine with the current state.
In reply to: 163074706 [](ancestors = 163074706,162479509)
Can we add tests with data that is "incorrect" in an array? (something like a tag in tags or server in servers), so that we can check that our Jsonpointer looks correct in that case #Closed Refers to: test/Microsoft.OpenApi.Tests/Services/OpenApiValidatorTests.cs:56 in 7e1f151. [](commit_id = 7e1f151, deletion_comment = False) |
| { | ||
| /// <summary> | ||
| /// Open API visitor base providing base validation logic for each <see cref="IOpenApiElement"/> | ||
| /// Open API visitor base provides common logic for concrete vistors |
There was a problem hiding this comment.
vistors [](start = 65, length = 7)
visitors
| @@ -0,0 +1,229 @@ | |||
| using System.Collections.Generic; | |||
| } | ||
| } | ||
|
|
||
| public class LocatorVisitor : OpenApiVisitorBase |
There was a problem hiding this comment.
This is actually a pretty nice feature. What do you think about moving it to the source, instead of leaving it in tests?
There was a problem hiding this comment.
This doesn't have to be done with this PR though. Maybe let's change this to private and we can decide?
| _visitor.Exit(); | ||
| foreach (var pathItem in paths) | ||
| { | ||
| InContext(pathItem.Key.Replace("/", "~1"), () => Walk(pathItem.Value));// JSON Pointer uses ~1 as an escape character for / |
There was a problem hiding this comment.
This "Replace" should technically apply to anything in the JSON pointer (not just pathItem names). Can we move it to the InContext method?
| _visitor.Visit(item); | ||
| foreach (var item in encodings.Values) | ||
| { | ||
| _visitor.Visit(item); |
There was a problem hiding this comment.
Should this be InContext?
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds a segment to the context path to enable pointing to the current locationin the document |
There was a problem hiding this comment.
nit, space missing between location and in
| /// </summary> | ||
| /// <param name="context">An identifier for the context.</param> | ||
| /// <param name="walk">An action that walks objects within the context.</param> | ||
| private void InContext(string context, Action walk) |
There was a problem hiding this comment.
nit, InContext is a little strange as a method name.
This is still a Walk method to me (just with a context name and a generic walk that is not tied to an object). Maybe it should be called Walk?
| { | ||
| private readonly OpenApiVisitorBase _visitor; | ||
| private readonly Stack<OpenApiSchema> _schemaLoop = new Stack<OpenApiSchema>(); | ||
| private readonly Stack<OpenApiPathItem> _pathItemLoop = new Stack<OpenApiPathItem>(); |
There was a problem hiding this comment.
Wow. I didn't realize before today that pathItem can also cause cycles (via Operation -> Callback)!
| foreach (var item in components.Schemas) | ||
| { | ||
| InContext(item.Key, () => Walk(item.Value)); | ||
| } |
There was a problem hiding this comment.
Each of these loops should be in another InContext:
InContext( OpenApiConstants.Schemas, () => foreach (var item in components.Schemas)
{
InContext(item.Key, () => Walk(item.Value));
} );
| _visitor.Exit(); | ||
| foreach (var mediaType in content) | ||
| { | ||
| InContext(mediaType.Key.Replace("/", "~1"), () => Walk(mediaType.Value)); |
There was a problem hiding this comment.
Let's move the Replace into InContext since it's used multiple times.
PerthCharern
left a comment
There was a problem hiding this comment.
Left a few more comments. Once those are addressed, I think we are good to go. :)
| Assert.NotNull(rules); | ||
| Assert.NotEmpty(rules); | ||
| Assert.Equal(13, rules.ToList().Count); // please update the number if you add new rule. | ||
| Assert.Equal(14, rules.ToList().Count); // please update the number if you add new rule. |
There was a problem hiding this comment.
I don't think this test is needed. It doesn't really add much value, and will cause maintenance issue (like the current test failure).
| @@ -0,0 +1,69 @@ | |||
| using FluentAssertions; | |||
|
I was really hoping to merge this today. But I cannot repro this failing test locally. Even running the Xunit console runner in release mode does not repro this fail. sigh. |
|
Maybe I misunderstood something but based on the AppVeyor TESTS tab, it looks like the failing test is the one counting number of rules.
I also left a comment earlier suggesting removing that test since it doesn’t really add much value and requires high maintenance.
Does this solve the problem, or are you seeing other tests failing on AppVeyor as well?
|
|
@PerthCharern I could remove it and that would make the problem go away, but I am concerned that for some reason when reflecting over the available Rules, we are picking up the FooExtension test that is not supposed to be in the set of default rules. I may remove the test temporarily to get these changes merged in, but I would really like to know why the problem is happening. |
|
@darrelmiller My mistake. I wasn't aware that the number increase was not intentional. For some reason, I thought you added a rule and simply did not update the number. |
|
@darrelmiller I also can't repro it locally. One thing that might be helpful: Make the test output something (using ITestOutputHelper) and we can see the output on AppVeyor. Given that we can't see the issue locally, I wonder if this has something to do with the lazy initialization with isThreadSafe being true. |
|
@PerthCharern Thanks for all the comments. I'll make the changes later this evening. I do think the test problem is a threading issue (although is very consistent!). Previously isThreadSafe was false and I changed it because from the docs it appears that setting it to true adds in a lock. The parameter isn't asking a question, it is a directive. I might try pre-reading the ruleset in the test setup to see if that makes the problem go away. |
|
@darrelmiller I think I know what's going on. I'll send out a PR today. |
Re-enable the default rule count test case (disabled in #176) The problem was this: ValidateCustomExtension test calls ValidationRuleSet.DefaultRuleSet and makes changes to it directly. Since the DefaultRuleSet is cached, the underlying private _defaultRuleSet is actually modified. The reason we did not see the issue locally might be that the order of local test run and the order on AppVeyor are different. We see the issue only if the "Count" test is run after the custom extension test.
This PR incorporates the other strongly typed extension work and allows defining validation rules for those extension types.