-
Notifications
You must be signed in to change notification settings - Fork 2
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
Feature/39 path matching bugfix #43
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,15 +5,20 @@ | |
namespace Menes | ||
{ | ||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Reflection.Metadata; | ||
using System.Text; | ||
using System.Text.Encodings.Web; | ||
using System.Text.RegularExpressions; | ||
using Corvus.Extensions; | ||
using Menes.Exceptions; | ||
using Menes.Internal; | ||
using Menes.Validation; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.OpenApi.Models; | ||
using Tavis.UriTemplates; | ||
|
@@ -41,6 +46,8 @@ namespace Menes | |
/// </remarks> | ||
public class OpenApiDocumentProvider : IOpenApiDocumentProvider | ||
{ | ||
private static readonly ConcurrentDictionary<OpenApiServer, Regex> RegexCache = new ConcurrentDictionary<OpenApiServer, Regex>(); | ||
|
||
private readonly IList<OpenApiPathTemplate> pathTemplates = new List<OpenApiPathTemplate>(); | ||
private readonly ILogger<OpenApiDocumentProvider> logger; | ||
private readonly List<OpenApiDocument> addedOpenApiDocuments; | ||
|
@@ -137,7 +144,7 @@ public void Add(OpenApiDocument document) | |
} | ||
|
||
this.addedOpenApiDocuments.Add(document); | ||
this.pathTemplates.AddRange(document.Paths.Select(path => new OpenApiPathTemplate(path.Key, path.Value))); | ||
this.pathTemplates.AddRange(document.Paths.Select(path => new OpenApiPathTemplate(path.Key, path.Value, document))); | ||
this.pathTemplatesByOperationId = null; | ||
if (this.logger.IsEnabled(LogLevel.Trace)) | ||
{ | ||
|
@@ -150,20 +157,27 @@ public bool GetOperationPathTemplate(string requestPath, string method, [NotNull | |
{ | ||
foreach (OpenApiPathTemplate template in this.pathTemplates) | ||
{ | ||
if (template.Match.IsMatch(requestPath)) | ||
Match match = template.Match.Match(requestPath); | ||
|
||
if (match.Success) | ||
{ | ||
if (template.PathItem.Operations.TryGetValue(method.ToOperationType(), out OpenApiOperation operation)) | ||
OpenApiServer? server = MatchServer(match, requestPath, template); | ||
|
||
if (server != null) | ||
{ | ||
this.logger.LogInformation( | ||
"Matched request [{method}] [{requestPath}] with template [{template}] to [{operation}]", | ||
method, | ||
requestPath, | ||
template.UriTemplate, | ||
operation.GetOperationId()); | ||
|
||
// This is the success path | ||
operationPathTemplate = new OpenApiOperationPathTemplate(operation, template); | ||
return true; | ||
if (template.PathItem.Operations.TryGetValue(method.ToOperationType(), out OpenApiOperation operation)) | ||
{ | ||
this.logger.LogInformation( | ||
"Matched request [{method}] [{requestPath}] with template [{template}] to [{operation}]", | ||
method, | ||
requestPath, | ||
template.UriTemplate, | ||
operation.GetOperationId()); | ||
|
||
// This is the success path | ||
operationPathTemplate = new OpenApiOperationPathTemplate(operation, template, server); | ||
return true; | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -176,5 +190,31 @@ public bool GetOperationPathTemplate(string requestPath, string method, [NotNull | |
operationPathTemplate = null; | ||
return false; | ||
} | ||
|
||
private static OpenApiServer? MatchServer(Match match, string requestPath, OpenApiPathTemplate template) | ||
{ | ||
string precursor = match.Index == 0 ? string.Empty : requestPath.Substring(0, match.Index); | ||
if (template.PathItem.Servers.Count > 0) | ||
{ | ||
return MatchPrecursor(precursor, template.PathItem.Servers); | ||
} | ||
|
||
if (template.Document?.Servers.Count > 0) | ||
{ | ||
return MatchPrecursor(precursor, template.Document.Servers); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
private static OpenApiServer? MatchPrecursor(string precursor, IList<OpenApiServer> servers) | ||
{ | ||
return servers.FirstOrDefault(s => | ||
{ | ||
Regex regex = RegexCache.GetOrAdd(s, new Regex(UriTemplate.CreateMatchingRegex2(s.Url), RegexOptions.Compiled)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have been unable to work out why
|
||
Match match = regex.Match(precursor); | ||
return match.Success && match.Index == 0; | ||
}); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,11 +18,13 @@ public class OpenApiPathTemplate | |
/// </summary> | ||
/// <param name="uriTemplate">The uri template.</param> | ||
/// <param name="item">The path item.</param> | ||
public OpenApiPathTemplate(string uriTemplate, OpenApiPathItem item) | ||
/// <param name="document">The Open API document in which this is a path template.</param> | ||
public OpenApiPathTemplate(string uriTemplate, OpenApiPathItem item, OpenApiDocument? document) | ||
{ | ||
this.UriTemplate = new UriTemplate(uriTemplate); | ||
this.PathItem = item; | ||
this.Match = new Regex(UriTemplate.CreateMatchingRegex(uriTemplate), RegexOptions.Compiled); | ||
this.Match = new Regex(UriTemplate.CreateMatchingRegex2(uriTemplate), RegexOptions.Compiled); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ...so you've found the need to change to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "2" |
||
this.Document = document; | ||
} | ||
|
||
/// <summary> | ||
|
@@ -39,5 +41,10 @@ public OpenApiPathTemplate(string uriTemplate, OpenApiPathItem item) | |
/// Gets the <see cref="Regex"/> which provides a match. | ||
/// </summary> | ||
public Regex Match { get; } | ||
|
||
/// <summary> | ||
/// Gets the <see cref="OpenApiDocument"/> containing this path template. | ||
/// </summary> | ||
public OpenApiDocument? Document { get; } | ||
} | ||
} |
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.
So...what is this? I think it's a lazily populated dictionary of regular expressions intended to match the
url
of a Server Object in an API spec. However, I think there may be a subtle problem here.I presume the reason you're using
RegEx
here, and not just matching withstring.StartsWith
is that the serverurl
property supports Server Variables, so it might look likehttps://{appname}.azurewebsites.net/{apiVersion}
. The OpenAPI spec apparently makes a decision of convenience here: in order to support using variables in this way, the URL is, syntactically speaking, a URL template. (They don't explicitly say that in theurl
property docs, but they do mention it in the Server Object'svariables
docs.)And so you've used
Tavis.UriTemplates
to build a RegEx that matches strings against URL templates. Sounds perfectly sensible. Except I don't think it'll do what you meant.Take an even simpler example than the one I gave:
https://{appname}.azurewebsites.net/
. That will match literally any Azure web app. I don't think that's what you actually mean. And let me give an example of why.In
OpenApiDocumentProvider.feature
you have this:and you include this line in the examples:
| No matching path | https://duff.server.io/v2/pet/65 | GET |
I venture to suggest that if I were to add this similar line:
| No matching path | https://duff.swagger.io/v2/pet/65 | GET |
you would also expect the test to pass (i.e., it the code under test should fail in the way the test says it should fail) for exactly the same reason the previous example fails: the server name is wrong here.
And indeed it does fail. But only because your example OpenAPI spec doesn't hit the problem I'm describing. Now consider changing the OpenAPI spec by changing the first server's
url
thus:(And this is exactly how this feature of OpenAPI is meant to be used as far as I can tell.) The presence of that
enum
line constrains this to be one of three possible values, meaning that there are exactly three possible roots for thehttps
form here. That means that whilehttps://petstore.swagger.io/v2/pet/65
,https://petstoredev.swagger.io/v2/pet/65
, andhttps://petstoretest.swagger.io/v2/pet/65
test are all valid,https://duff.swagger.io/v2/pet/65
definitely isn't.However, with the YAML set up in that way, and with the extra test scenario example added as shown above, the test now fails, because the
Regex
you end up creating here is effectively matchinghttps://*.swagger.io/v2
.That is the correct logic for cases where a URI Template is being used in the most usual way, i.e., to match anything that conforms to the template. But it's not what's required here.
It's not entirely clear to me what is really required here. I'm not sure whether the intention is that individual environments can say what value they want to plug in for each server variable, or whether it may actually be valid for a single environment to be able to specify multiple values for each variable, and for things to match any time the URL matches for any combination of the variables.
But what does seem clear is that the current behaviour is demonstrably wrong: it will accept any value in a template placeholder even when the relevant Open API spec clearly states that this is not allowed (as in my example above).
So my suggestion for now would be simply not to support variable-based server URLs, and to revisit this if we ever turn out to need it.
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.
Right - this is exactly what I didn't fully understand about the intended use of those variables.
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 think it would be better just to support matching the possible variable substitutions. I also note that I have missed the fact that there are Operation overrides as well as Path overrides for the servers and so I need to support both of those cases too.
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 agree with your suggestion to defer this to "a later date" - the later date being imminent on the type generation branch.