Skip to content

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

Merged
merged 26 commits into from
Jan 31, 2018
Merged

Conversation

darrelmiller
Copy link
Member

This PR incorporates the other strongly typed extension work and allows defining validation rules for those extension types.


/// <summary>
///
Copy link
Contributor

@xuzhg xuzhg Jan 13, 2018

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)
Copy link
Contributor

@xuzhg xuzhg Jan 13, 2018

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

Copy link
Member Author

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>
///
Copy link
Contributor

@xuzhg xuzhg Jan 13, 2018

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)
Copy link
Contributor

@xuzhg xuzhg Jan 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public? not internal? #WontFix

Copy link
Member Author

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;

Copy link
Contributor

@xuzhg xuzhg Jan 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove #Resolved

@PerthCharern
Copy link
Contributor

PerthCharern commented Jan 18, 2018

using Microsoft.OpenApi.Writers;

nit, add copyright #Closed


Refers to: src/Microsoft.OpenApi/Interfaces/IOpenApiExtension.cs:1 in 7e1f151. [](commit_id = 7e1f151, deletion_comment = False)

private readonly Stack<string> _path = new Stack<string>();

/// <summary>
/// Allow Rule to indicate validation error occured a deeper context level.
Copy link
Contributor

@PerthCharern PerthCharern Jan 18, 2018

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still missing :)


In reply to: 162469699 [](ancestors = 162469699)

Copy link
Member Author

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"));
Copy link
Contributor

@PerthCharern PerthCharern Jan 18, 2018

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);
}
Copy link
Contributor

@PerthCharern PerthCharern Jan 18, 2018

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);
}
Copy link
Contributor

@PerthCharern PerthCharern Jan 18, 2018

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);
Copy link
Contributor

@PerthCharern PerthCharern Jan 18, 2018

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

Copy link
Contributor

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)

Copy link
Contributor

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);
}
Copy link
Contributor

@PerthCharern PerthCharern Jan 18, 2018

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

@PerthCharern
Copy link
Contributor

PerthCharern commented Jan 18, 2018

using System;

nit, copyright, and how is this file used? #Closed


Refers to: src/Microsoft.OpenApi/Services/VisitorContext.cs:1 in 7e1f151. [](commit_id = 7e1f151, deletion_comment = False)

return _errors;
}
}
void Exit();
Copy link
Contributor

@PerthCharern PerthCharern Jan 18, 2018

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

Copy link
Member Author

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
Copy link
Contributor

@PerthCharern PerthCharern Jan 18, 2018

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?

Copy link
Member Author

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)

Copy link
Contributor

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)

@PerthCharern
Copy link
Contributor

PerthCharern commented Jan 18, 2018

}

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)

Copy link
Contributor

@PerthCharern PerthCharern left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕐

@darrelmiller
Copy link
Member Author

using System;

I deleted it.


In reply to: 358791618 [](ancestors = 358791618)


Refers to: src/Microsoft.OpenApi/Services/VisitorContext.cs:1 in 7e1f151. [](commit_id = 7e1f151, deletion_comment = False)

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
Copy link
Contributor

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;
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor

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 /
Copy link
Contributor

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);
Copy link
Contributor

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
Copy link
Contributor

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)
Copy link
Contributor

@PerthCharern PerthCharern Jan 29, 2018

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>();
Copy link
Contributor

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));
}
Copy link
Contributor

@PerthCharern PerthCharern Jan 29, 2018

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));
Copy link
Contributor

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.

Copy link
Contributor

@PerthCharern PerthCharern left a 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.
Copy link
Contributor

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, copyright

@darrelmiller
Copy link
Member Author

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.

@PerthCharern
Copy link
Contributor

PerthCharern commented Jan 30, 2018 via email

@darrelmiller
Copy link
Member Author

@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.

@PerthCharern
Copy link
Contributor

@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.

@PerthCharern
Copy link
Contributor

@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.

@darrelmiller
Copy link
Member Author

@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 darrelmiller merged commit 4118028 into master Jan 31, 2018
@darrelmiller darrelmiller deleted the dm/validatedextensions branch January 31, 2018 15:52
@PerthCharern
Copy link
Contributor

@darrelmiller I think I know what's going on. I'll send out a PR today.

PerthCharern added a commit that referenced this pull request Jan 31, 2018
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants