-
Notifications
You must be signed in to change notification settings - Fork 262
Validation for custom extensions #176
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
||
/// <summary> | ||
/// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fill? #Resolved
/// | ||
/// </summary> | ||
/// <param name="settings"></param> | ||
public OpenApiStreamReader(OpenApiReaderSettings settings = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's better to separate two constructors? #WontFix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)
@@ -14,5 +15,23 @@ public class OpenApiObject : Dictionary<string, IOpenApiAny>, IOpenApiAny | |||
/// Type of <see cref="IOpenApiAny"/>. | |||
/// </summary> | |||
public AnyType AnyType { get; } = AnyType.Object; | |||
|
|||
/// <summary> | |||
/// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fill? #Resolved
/// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public? not internal? #WontFix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove #Resolved
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[](start = 59, length = 1)
nit, preposition missing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now it's fixed :-)
{ | ||
Walk(pathItem); | ||
_visitor.Enter(pathItem.Key.Replace("/","~1")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
~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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
@@ -46,12 +48,15 @@ public void Walk(OpenApiDocument doc) | |||
/// <param name="tags"></param> | |||
internal void Walk(IList<OpenApiTag> tags) | |||
{ | |||
_visitor.Enter(OpenApiConstants.Tags); | |||
_visitor.Visit(tags); | |||
|
|||
foreach (var tag in tags) | |||
{ | |||
Walk(tag); | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
@@ -46,12 +48,15 @@ public void Walk(OpenApiDocument doc) | |||
/// <param name="tags"></param> | |||
internal void Walk(IList<OpenApiTag> tags) | |||
{ | |||
_visitor.Enter(OpenApiConstants.Tags); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I opened another comment to discuss this.
In reply to: 162478481 [](ancestors = 162478481,162477655)
@@ -224,11 +274,13 @@ internal void Walk(IList<OpenApiParameter> parameters) | |||
{ | |||
if (parameters != null) | |||
{ | |||
_visitor.Enter(OpenApiConstants.Parameters); | |||
_visitor.Visit(parameters); | |||
foreach (var parameter in parameters) | |||
{ | |||
Walk(parameter); | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need indexing - see my other comments. #Closed
return _errors; | ||
} | ||
} | ||
void Exit(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are these needed in the context? #Closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)
|
||
namespace Microsoft.OpenApi.Validations | ||
{ | ||
/// <summary> | ||
/// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
, 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🕐
using Microsoft.OpenApi.Interfaces; | ||
using Microsoft.OpenApi.Models; | ||
|
||
namespace Microsoft.OpenApi.Services | ||
{ | ||
/// <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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
vistors [](start = 65, length = 7)
visitors
@@ -0,0 +1,229 @@ | |||
using System.Collections.Generic; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, copyright
} | ||
} | ||
|
||
public class LocatorVisitor : OpenApiVisitorBase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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?
@@ -16,6 +16,8 @@ namespace Microsoft.OpenApi.Services | |||
public class OpenApiWalker | |||
{ | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's move the Replace into InContext since it's used multiple times.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left a few more comments. Once those are addressed, I think we are good to go. :)
@@ -34,7 +34,7 @@ public void DefaultRuleSetPropertyReturnsTheCorrectRules() | |||
// Assert | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, copyright
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.