Skip to content
This repository was archived by the owner on Nov 16, 2023. It is now read-only.

Add predefined operationId support. #233

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System;

namespace Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Exceptions
{
/// <summary>
/// The exception that is recorded when it is expected to have operationId XML tag
/// but it is missing or there are more than one tags.
/// </summary>
[Serializable]
internal class InvalidOperationIdException : DocumentationException
{
/// <summary>
/// Initializes a new instance of the <see cref="InvalidOperationIdException"/>.
/// </summary>
/// <param name="message">Error message.</param>
public InvalidOperationIdException(string message = "")
: base(message)
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -245,15 +245,5 @@ public static string ToTitleCase(this string value)

return value.Substring(startIndex: 0, length: 1).ToUpperInvariant() + value.Substring(startIndex: 1);
}

/// <summary>
/// Extracts the absolute path from a full URL string.
/// </summary>
/// <param name="value">The string in URL format.</param>
/// <returns>The absolute path inside the URL.</returns>
public static string UrlStringToAbsolutePath(this string value)
{
return WebUtility.UrlDecode(new Uri(value).AbsolutePath);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public static FilterSet GetDefaultFilterSet(FilterSetVersion version)
_defaultFilterSet.Add(new PopulateInAttributeFilter());
_defaultFilterSet.Add(new ConvertAlternativeParamTagsFilter());
_defaultFilterSet.Add(new ValidateInAttributeFilter());
_defaultFilterSet.Add(new BranchOptionalPathParametersFilter());
_defaultFilterSet.Add(new CreateOperationMetaFilter());

//Post processing document filters
_defaultFilterSet.Add(new RemoveFailedGenerationOperationFilter());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ private IList<OperationGenerationDiagnostic> GenerateSpecificationDocuments(

try
{
operationMethod = OperationHandler.GetOperationMethod(url, operationElement);
operationMethod = OperationHandler.GetOperationMethod(operationElement);
}
catch (InvalidVerbException e)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public class KnownXmlStrings
public const string AuthorizationCode = "authorizationCode";
public const string ClientCredentials = "clientCredentials";
public const string Scope = "scope";
public const string OperationId = "operationId";
public static string[] AllowedAppKeyInValues => new[] {Header, Query, Cookie};

public static string[] AllowedFlowTypeValues => new[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,42 @@ namespace Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration
internal static class OperationHandler
{
/// <summary>
/// Gets the operation id by parsing segments out of the absolute path.
/// Checks whether the optional operation id tag is present in the operation element.
/// </summary>
public static string GetOperationId(string absolutePath, OperationType operationMethod)
/// <param name="operationElement"></param>
/// <returns>True if the operationId tag is in the operation element, otherwise false.</returns>
public static bool HasOperationId(XElement operationElement)
{
return operationElement?.Elements().Where(p => p.Name == KnownXmlStrings.OperationId).Count() > 0;
}

/// <summary>
/// Extracts the operation id from the operation element.
/// </summary>
/// <param name="operationElement"></param>
/// <returns>Operation id.</returns>
/// <exception cref="InvalidOperationIdException">Thrown if the operationId tag is missing or
/// there are more than one tags.</exception>
public static string GetOperationId(XElement operationElement)
{
var operationIdList = operationElement?.Elements().Where(p => p.Name == KnownXmlStrings.OperationId).ToList();
if (operationIdList?.Count == 1)
{
return operationIdList[0].Value;
}
else
{
string error = operationIdList.Count > 1
? SpecificationGenerationMessages.MultipleOperationId
: SpecificationGenerationMessages.NoOperationId;
throw new InvalidOperationIdException(error);
}
}

/// <summary>
/// Generates the operation id by parsing segments out of the absolute path.
/// </summary>
public static string GenerateOperationId(string absolutePath, OperationType operationMethod)
{
var operationId = new StringBuilder(operationMethod.ToString().ToLowerInvariant());

Expand Down Expand Up @@ -63,10 +96,9 @@ public static string GetOperationId(string absolutePath, OperationType operation
/// <summary>
/// Extracts the operation method from the operation element
/// </summary>
/// <param name="operationElement">The xml element representing an operation in the annotation xml.</param>
/// <exception cref="InvalidVerbException">Thrown if the verb is missing or has invalid format.</exception>
public static OperationType GetOperationMethod(
string url,
XElement operationElement)
public static OperationType GetOperationMethod(XElement operationElement)
{
var verbElement = operationElement.Descendants().FirstOrDefault(i => i.Name == KnownXmlStrings.Verb);

Expand All @@ -85,6 +117,7 @@ public static OperationType GetOperationMethod(
/// <summary>
/// Extracts the URL from the operation element
/// </summary>
/// <param name="operationElement">The xml element representing an operation in the annotation xml.</param>
/// <exception cref="InvalidUrlException">Thrown if the URL is missing or has invalid format.</exception>
public static string GetUrl(
XElement operationElement)
Expand All @@ -103,7 +136,7 @@ public static string GetUrl(

try
{
url = WebUtility.UrlDecode(new Uri(url).AbsolutePath);
url = WebUtility.UrlDecode(new Uri(WebUtility.UrlDecode(url)).AbsolutePath);
}
catch (UriFormatException)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,18 @@ namespace Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.PreprocessingOp
/// Parses the value of the URL and creates multiple operations in the Paths object when
/// there are optional path parameters.
/// </summary>
public class BranchOptionalPathParametersFilter : IPreProcessingOperationFilter
public class BranchOptionalPathParametersFilter : ICreateOperationPreProcessingOperationFilter
{
/// <summary>
/// Verifies that the annotation XML element contains all data which are required to apply this filter.
/// </summary>
/// <param name="element">The xml element representing an operation in the annotation xml.</param>
/// <returns>Always true (this filter can be always applied).</returns>
public bool IsApplicable(XElement element)
{
return true;
}

/// <summary>
/// Fetches the URL value and creates multiple operations based on optional parameters.
/// </summary>
Expand All @@ -37,30 +47,18 @@ public IList<GenerationError> Apply(

try
{
var paramElements = element.Elements()
var paramPathElements = element.Elements()
.Where(
p => p.Name == KnownXmlStrings.Param)
.Where(
p => p.Attribute(KnownXmlStrings.In)?.Value == KnownXmlStrings.Path)
.ToList();

// We need both the full URL and the absolute paths for processing.
// Full URL contains all path and query parameters.
// Absolute path is needed to get OperationId parsed out correctly.
var fullUrl = element.Elements()
.FirstOrDefault(p => p.Name == KnownXmlStrings.Url)
?.Value;

var absolutePath = fullUrl.UrlStringToAbsolutePath();
var absolutePath = OperationHandler.GetUrl(element);

var operationMethod = (OperationType)Enum.Parse(
typeof(OperationType),
element.Elements().FirstOrDefault(p => p.Name == KnownXmlStrings.Verb)?.Value,
ignoreCase: true);
var allGeneratedPathStrings = GeneratePossiblePaths(absolutePath, paramPathElements);

var allGeneratedPathStrings = GeneratePossiblePaths(
absolutePath,
paramElements.Where(
p => p.Attribute(KnownXmlStrings.In)?.Value == KnownXmlStrings.Path)
.ToList());
var operationMethod = OperationHandler.GetOperationMethod(element);

foreach (var pathString in allGeneratedPathStrings)
{
Expand All @@ -72,7 +70,7 @@ public IList<GenerationError> Apply(
paths[pathString].Operations[operationMethod] =
new OpenApiOperation
{
OperationId = OperationHandler.GetOperationId(pathString, operationMethod)
OperationId = OperationHandler.GenerateOperationId(pathString, operationMethod)
};
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
// ------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Xml.Linq;
using Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Models;
using Microsoft.OpenApi.Models;

namespace Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.PreprocessingOperationFilters
{
/// <summary>
/// Filter to initialize OpenApi operation based on the annotation XML element.
///
/// It does not do the creation itself but forwards the call to the real generator filters.
/// The first filter which is applicable is executed in order to generate the operation.
/// </summary>
public class CreateOperationMetaFilter : IPreProcessingOperationFilter
{
private List<ICreateOperationPreProcessingOperationFilter> createOperationFilters;

/// <summary>
/// Initializes a new production instance of the <see cref="CreateOperationMetaFilter"/>.
/// </summary>
/// <remarks>
/// Using this constructor, the following filter list is used:
/// If the XML element contains 'operationId' tag, it is used as unique identifier
/// of the operation. Otherwise, the id is generated using the path of the operation.
/// In the latter case, multiple operation could be generated, if the path has optional
/// parameters.
/// </remarks>
public CreateOperationMetaFilter() :
this(new List<ICreateOperationPreProcessingOperationFilter> {
new UsePredefinedOperationIdFilter(), new BranchOptionalPathParametersFilter()})
{
}

/// <summary>
/// Initializes a new instance of the <see cref="CreateOperationMetaFilter"/>.
/// </summary>
/// <param name="createOperationFilters">List of generator filters.</param>
internal CreateOperationMetaFilter(
List<ICreateOperationPreProcessingOperationFilter> createOperationFilters)
{
this.createOperationFilters = createOperationFilters;
}

/// <summary>
/// Initializes the OpenApi operation based on the annotation XML element.
/// It applies the first applicable filter to create operations.
/// </summary>
/// <param name="paths">The paths to be updated.</param>
/// <param name="element">The xml element representing an operation in the annotation xml.</param>
/// <param name="settings">The operation filter settings.</param>
/// <returns>The list of generation errors, if any produced when processing the filter.</returns>
public IList<GenerationError> Apply(
OpenApiPaths paths,
XElement element,
PreProcessingOperationFilterSettings settings)
{
foreach (var filter in this.createOperationFilters)
{
if (filter.IsApplicable(element))
{
return filter.Apply(paths, element, settings);
}
}

// If none of the filters could be applied --> error
return new List<GenerationError>
{
new GenerationError
{
Message = "Failed to apply any operation creation filter.",
ExceptionType = nameof(InvalidOperationException)
}
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
// ------------------------------------------------------------

using System.Collections.Generic;
using System.Xml.Linq;
using Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Models;
using Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.OperationFilters;
using Microsoft.OpenApi.Models;

namespace Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.PreprocessingOperationFilters
{
/// <summary>
/// The class representing the contract of a filter to preprocess the <see cref="OpenApiOperation"/>
/// objects in <see cref="OpenApiPaths"/> before each <see cref="OpenApiOperation"/> is processed by the
/// <see cref="IOperationFilter"/>.
/// </summary>
public interface ICreateOperationPreProcessingOperationFilter : IPreProcessingOperationFilter
{
/// <summary>
/// Verifies that the annotation XML element contains all data which are required to apply this filter.
/// </summary>
/// <param name="element">The xml element representing an operation in the annotation xml.</param>
/// <returns>True if the filter can be applied, otherwise false.</returns>
bool IsApplicable(XElement element);
}
}
Loading