Skip to content

Conversation

@darrelmiller
Copy link
Member

@darrelmiller darrelmiller commented Dec 20, 2017

This uses the Rules from @xuzhg 's PR but my Validator and Walker. The tests that @xuzhg created have been updated to use the Validator instead.

There are still some parts of the DOM graph that are not yet being visited. Walker tests will need to be implemented.

What I believe are the advantages of this approach:

  • Walker can be re-used for purposes other than validation
  • A new Visitor can be created as easily as just creating one class and overloading the methods you want to visit.
  • Visitor classes are replaced by a set of methods on a Single Visitor class
  • The Validation visitor class plays the role of VisitorSet

It is almost 1000 LOC less :-)

var validator = new OpenApiValidator();
validator.Visit(components);
// Act
bool result = components.Validate(out errors);
Copy link
Contributor

Choose a reason for hiding this comment

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

Now, the developer should create an object of OpenApiValidator and check the validator.Errors?

Why do you think the three line codes is better than the one line code call? components.Validate(out errors)?

@PerthCharern what do you think?

/// <summary>
/// The interface for visitor.
/// </summary>
internal interface IVisitor
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe I rename IVistor as IValidator can help to understand?

/// </summary>
/// <param name="doc"></param>
/// <param name="doc">OpenApiDocument to be walked</param>
public void Walk(OpenApiDocument doc)
Copy link
Contributor

Choose a reason for hiding this comment

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

That's my concern. Does the customer only walk starting from OpenApiDocument?

Copy link
Member Author

Choose a reason for hiding this comment

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

If you look at the tests, you can Visit an element directly. If we wanted to expose Walk entry points for sub-graphs we could do that too. But then in your next comment you suggest that OpenApiValidator should be internal.

Copy link
Contributor

Choose a reason for hiding this comment

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

Calling Visit is not the same as calling Walk though. Visit doesn't recursively walk into the children, right? I think it's still nice to provide a Walk-like feature at every level of the subgraph.


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

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure it would be nice, but exposing functionality publicly that people can take a dependency on before we have any real experience with how well the validation works seems risky to me. Why don't we get the validation working as an internal feature and once we are comfortable with it, then consider exposing more. Unless we have a concrete usecase, I would rather not increase our support surface area.


In reply to: 158124882 [](ancestors = 158124882,158121892)

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you know of use cases for doing anything else?


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

Copy link
Contributor

@PerthCharern PerthCharern Dec 20, 2017

Choose a reason for hiding this comment

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

One possible use case I can envision:

  • Define a large reusable component (say, schema) in
    a separate file and refer to it using Reference. User wants to make sure this component is defined correctly by running a validation on this component (schema), which is technically not in a document.

In reply to: 158125996 [](ancestors = 158125996,158115415)

Copy link
Member Author

Choose a reason for hiding this comment

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

The only minor problem we have with that at the moment is we don't support reading anything other than a complete document via our public interface. We will need to do that internally when pulling in external references.


In reply to: 158127399 [](ancestors = 158127399,158125996,158115415)

Copy link
Contributor

Choose a reason for hiding this comment

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

You are right. This subgraph-level discussion seems to be a recurring theme (writer, reader, and now walker).

I agree with you on this:

"It is worth noting that is much easier to add more granularity in a future release than to take it away."

So I think this level of exposure is fine as is as a beginning point.

On that note, there might be value in adding the Walk methods for all leaf nodes. In case we would like to flip the internal/public switch in the future, it will be a simple string change.


In reply to: 158128738 [](ancestors = 158128738,158127399,158125996,158115415)

/// <summary>
/// Class containing dispatchers to execute validation rules on for Open API document.
/// </summary>
public class OpenApiValidator : OpenApiVisitorBase
Copy link
Contributor

Choose a reason for hiding this comment

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

Normally, the OpenApiValidator should be internal.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it needs to be public in this implementation though. The user needs to instantiate OpenApiWalker with new OpenApiValidator() as an argument to perform validation.


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

Copy link
Contributor

Choose a reason for hiding this comment

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

@PerthCharern Sorry, Yes. In Darrel PR, it's public. But I mean it should be internal if we have a OpenApiValidator.

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'm not sure how we want to incorporate the Validator. We could make it an internal implementation detail, or we could allow users to initiate it directly via the Validator visitor.

Copy link
Member Author

Choose a reason for hiding this comment

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

If we can decide what are the use cases for validating fragments of the OpenApiDocument then we can decide what portions of the traversal algorithm we should expose as public. It is worth noting that is much easier to add more granularity in a future release than to take it away.


_visitor.Visit(doc as IOpenApiExtensible);

}
Copy link
Contributor

@PerthCharern PerthCharern Dec 20, 2017

Choose a reason for hiding this comment

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

It took me a bit to understand how to distinguish the case when we simply call .Visit and the case where we need to have WalkXXX private method.

Would it be easier to read if we always create WalkXXX method for every OpenApiXXX and its collection properties? If it's something simple without further inner object like OpenApiInfo, then sure, WalkInfo will simply be a wrapper around _visitor.Visit(info).

My concern is that with the current implementation, in case someone mistakenly uses .Visit instead of Walk in certain places, it will be very difficult to spot.

If we always create a WalkXXX, the pattern for each WalkXXX is also always consistent, with one Visit call and a bunch of following Walk calls.

public void WalkObject(OpenApiObject object)
{
    // Visit self
    _visitor.Visit(object);
    
     // Walk the rest
     WalkProperty1(object.Property1);
     WalkMapProperty2(object.MapProperty2);
}

public void WalkProperty1(OpenApiProperty1Type property1)
{
     // Visit self
     _visitor.Visit(property1);
     
     // Walk the rest
     WalkProperty11(property1.Property11);
     WalkProperty12(property1.Property12);
     WalkProperty13(property1.Property13);
}

public void WalkMapProperty2(IDictionary<string, Property2Type> property2Map)
{
     // Visit self
     _visitor.Visit(property2Map);
     
     // Walk the rest
     foreach( var property2 in property2Map.Values )
     {
           WalkProperty2(property2);
     }
}

This will also address Sam's concern about having other starting point for Walking.

Copy link
Member Author

Choose a reason for hiding this comment

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

Walk methods are just helper methods for grouping visits. Think of Walk methods as non-leaf nodes of the traversal tree and Visit as the leaf nodes. If you want to have a Walk for every leaf I suppose we could.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's a good thing to have. Say, if customer wants to only walk his OpenApiComponents, having a WalkComponents will enable that feature.


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

Copy link
Member Author

Choose a reason for hiding this comment

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

You mean they only want to Validate the components, but not validate anything else? Or do you mean they want to only apply a visitor to the components? If so, they can create a class that derives from the VisitorBase, create an overload of the Visit(OpenApiComponent ) method and then use the regular walk.


In reply to: 158121661 [](ancestors = 158121661,158121152)

Copy link
Contributor

Choose a reason for hiding this comment

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

I meant the former. Given that the implementation consistency concern is relatively minor, this boils down to the same issue as Sam's.

We can just discuss in the other thread in OpenApiWalker.cs to avoid repetition.


In reply to: 158126857 [](ancestors = 158126857,158121661,158121152)

@darrelmiller
Copy link
Member Author

Ok, @xuzhg @PerthCharern I hear your concerns. How about we meet in the middle? I will create Walk methods for every type that Visits the object and all child objects. But for the moment, we keep those methods internal. At least until we a) have specific case that requires us to make them public and b) we have gained a bit of confidence in the effectiveness of the approach.

I will make these changes and then we can make the final call as to whether we want take my approach of having all the visiting/walking methods in a single class, or Sam's approach of having a seperate class for each visitor.

Does that seem reasonable?

@PerthCharern
Copy link
Contributor

Yes, I didn't see this comment, so I just proposed the same thing in the thread. :) This sounds good to me.

/// Execute validation rules against an <see cref="OpenApiSchema"/>
/// </summary>
/// <param name="item">The object to be validated</param>
public override void Visit(OpenApiSchema item) => Validate(item);
Copy link
Contributor

Choose a reason for hiding this comment

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

item [](start = 49, length = 4)

schema

/// Execute validation rules against an <see cref="OpenApiEncoding"/>
/// </summary>
/// <param name="item">The object to be validated</param>
public override void Visit(OpenApiEncoding item) => Validate(item);
Copy link
Contributor

Choose a reason for hiding this comment

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

item [](start = 51, length = 4)

encoding

{
_visitor.Visit(tag);
_visitor.Visit(tag.ExternalDocs);
_visitor.Visit(tag as IOpenApiExtensible);
Copy link
Contributor

Choose a reason for hiding this comment

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

These should be Walk, right?

{
_visitor.Visit(variable);
}
_visitor.Visit(server as IOpenApiExtensible);
Copy link
Contributor

Choose a reason for hiding this comment

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

These should be Walk, right?

_visitor.Visit(pathItem);

Walk(pathItem.Operations);
_visitor.Visit(pathItem as IOpenApiExtensible);
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be Walk, right?

}
}

_visitor.Visit(response as IOpenApiExtensible);
Copy link
Contributor

Choose a reason for hiding this comment

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

These should be Walk, right?

{
foreach (var item in encoding.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.

_visitor.Visit(item); [](start = 14, length = 23)

Walk?

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.

:shipit:

@PerthCharern
Copy link
Contributor

I prefer this pattern over the current implementation in #152 since it separates the logic of walking and validating.

One concern that Sam raised earlier about the call pattern still remains unsolved:

In #152, the call pattern is simply:

            bool result = response.Validate(out errors);

But here, it's a bit more complicated and less intuitive:

            var validator = new OpenApiValidator();
            var walker = new OpenApiWalker(validator);
            walker.Walk(response);

As a customer, I would not like to repeat the three lines over and over. It also takes away the meaning of validation from the call line itself (i.e. it's easy to understand that response.Validate is performing validation, while it's not apparent what walker.Walk(response) attempts to achieve.).

What do you think about creating extension method for IOpenApiElement that takes care of calling the three lines?

@darrelmiller
Copy link
Member Author

@PerthCharern I have created the extension method, but needed to do a giant type switch to dispatch the walker. Should push that change soon.

@darrelmiller
Copy link
Member Author

@PerthCharern The Validate method isn't quite the same as @xuzhg proposed. I changed it to this way because the visitor (in this case OpenApiValidator) is responsible for the state during the walking of the subgraph. I don't really like the idea of passing external state (an existing list of errors) into the visitor. A consumer can always aggregate errors themselves. If someone wants to aggregate errors across different parts of the graph, then they might as well create their own custom walker.

Calling Validate and getting a list of errors back seems like a simpler interface for the case when someone wants to validate a subgraph.

Thoughts?

@PerthCharern
Copy link
Contributor

I'm okay with either design.

I don't think Sam's design was originally meant for aggregating errors. His design kinda resembles what we have in the reader (diagnostic passed as out param). We don't really need that here since there's only one return object, and I don't foresee us having more "outputs" for Validate.


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

/// <typeparam name="T"></typeparam>
/// <param name="element"></param>
/// <returns></returns>
public static IEnumerable<ValidationError> Validate(this IOpenApiElement element) {
Copy link
Contributor

Choose a reason for hiding this comment

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

So, for the customer/developer, he will check:

  1. The return is not null
  2. The return has elements.
OpenApiDocument doc = new OpenApiDocument();
var errors = doc.Validate();

if (errors == null || !errors.Any())
{
    // validation passed.
}
else
{
   // validation failed.
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Checking errors.Any() should be sufficient because returning IEnumerable as null is just evil. I will add documentation to convey the fact that the function will never return null.

@PerthCharern
Copy link
Contributor

@darrelmiller Are you waiting for anything from me or Sam? I think the overall structure looks good. We can help fill in the validation details in future PRs once this is checked in.

@darrelmiller darrelmiller merged commit ba575c4 into master Jan 11, 2018
@darrelmiller darrelmiller deleted the dm/validation branch January 4, 2020 20:27
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.

4 participants