This repository was archived by the owner on Nov 16, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 35
/
Copy pathBranchOptionalPathParametersFilter.cs
220 lines (190 loc) · 9.17 KB
/
BranchOptionalPathParametersFilter.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
// ------------------------------------------------------------
// 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.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Extensions;
using Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Models;
using Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.Models.KnownStrings;
using Microsoft.OpenApi.Models;
namespace Microsoft.OpenApi.CSharpAnnotations.DocumentGeneration.PreprocessingOperationFilters
{
/// <summary>
/// Parses the value of the URL and creates multiple operations in the Paths object when
/// there are optional path parameters.
/// </summary>
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>
/// <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)
{
var generationErrors = new List<GenerationError>();
try
{
var paramPathElements = element.Elements()
.Where(
p => p.Name == KnownXmlStrings.Param)
.Where(
p => p.Attribute(KnownXmlStrings.In)?.Value == KnownXmlStrings.Path)
.ToList();
var absolutePath = OperationHandler.GetUrl(element);
var allGeneratedPathStrings = GeneratePossiblePaths(absolutePath, paramPathElements);
var operationMethod = OperationHandler.GetOperationMethod(element);
foreach (var pathString in allGeneratedPathStrings)
{
if (!paths.ContainsKey(pathString))
{
paths[pathString] = new OpenApiPathItem();
}
paths[pathString].Operations[operationMethod] =
new OpenApiOperation
{
OperationId = OperationHandler.GenerateOperationId(pathString, operationMethod)
};
}
}
catch(Exception ex)
{
generationErrors.Add(
new GenerationError
{
Message = ex.Message,
ExceptionType = ex.GetType().Name
});
}
return generationErrors;
}
/// <summary>
/// Generates possible paths from a given path that may contain optional parameters.
/// </summary>
/// <param name="fullPath">The full path containing all optional and required parameters.</param>
/// <param name="pathParams">The path parameters, used to determine whether a path segment is optional.</param>
/// <returns>A list of possible paths.</returns>
public static List<string> GeneratePossiblePaths(string fullPath, IList<XElement> pathParams)
{
var savedGeneratedPaths = new List<List<int>>
{
new List<int>()
};
var paths = new List<string>();
var fullpathSegments = fullPath.Trim().Split('/');
// Track indices of segments in fullPathSegments that make up a possible path instead of
// the string path itself to make equality comparisons between segments straightforward
// E.g. fullpathSegments = ['v0', 'locales', '{tenant}', '{business}', '{app}', 'regions']
// then a possible generated path could be [0, 1, 2, 5]
// and the reconstructed path would be 'v0/locales/{tenant}/regions'.
for (var i = 0; i < fullpathSegments.Length; i++)
{
var generatedPaths = new List<List<int>>();
foreach (var path in savedGeneratedPaths)
{
// We want to add this segment to the current path if the path segment is not optional or
// if the last segment added to the path is also the last segment we saw.
if (!IsPathSegmentOptional(fullpathSegments[i], pathParams, fullPath) ||
path.LastOrDefault() == i - 1)
{
var newPath = new List<int>(path);
newPath.Add(i);
generatedPaths.Add(newPath);
}
// If the path segment is optional, then we need to include paths without it.
if (IsPathSegmentOptional(fullpathSegments[i], pathParams, fullPath))
{
generatedPaths.Add(new List<int>(path));
}
}
savedGeneratedPaths = generatedPaths;
}
// Reconstruct the string path from each list of indices.
foreach (var pathSegmentIndices in savedGeneratedPaths)
{
var path = string.Empty;
foreach (var pathSegmentIndex in pathSegmentIndices)
{
path += '/' + fullpathSegments[pathSegmentIndex];
}
// Remove the extra '/' in the front.
paths.Add(path.Substring(1));
}
return paths;
}
/// <summary>
/// Returns true if the segment of the path is a path parameter and is marked as optional
/// </summary>
/// <param name="pathSegment">
/// A segment of the path. For example, in the path
/// /v0/locales/{tenant}/{business}/{app}/regions,
/// v0, locales, {tenant}, {business}, {app}, and regions are all path segments.
/// </param>
/// <param name="pathParams">List of documented path parameters</param>
/// <param name="path">Full operation path</param>
/// <returns>Boolean representation of whether or not path segment is optional</returns>
private static bool IsPathSegmentOptional(string pathSegment, IList<XElement> pathParams, string path)
{
// Regex remove brackets from {pathParamNames} in segment
// Examples:
// {pathSegment} -> [pathSegment]
// productId:{productId}-skuId:{skuId} -> [productId, skuId]
var matches = new Regex(@"\{(.*?)\}").Matches(pathSegment);
// If no path params are found, the path segment is required
if (matches.Count == 0)
{
return false;
}
foreach (Match match in matches)
{
var pathParamName = match.Groups[1].Value;
// Find the path param
var pathParam = pathParams.FirstOrDefault(
p => string.Equals(
p.Attribute(KnownXmlStrings.Name)?.Value.Trim(),
pathParamName,
StringComparison.OrdinalIgnoreCase));
// All path params must be documented, so this is a mistake in the documentation.
// We will simply bail here and let the exception be thrown when
// PopulateInAttributeFilter is processed.
if (pathParam == null)
{
return false;
}
// If required attribute is not included, the segment defaults to required.
if (pathParam.Attribute(KnownXmlStrings.Required) == null)
{
return false;
}
bool required;
var parseSuccess = bool.TryParse(
pathParam.Attribute(KnownXmlStrings.Required)?.Value.Trim(),
out required);
// If any path parameter in the segment is marked as required, the entire segment is required.
if (!parseSuccess || required)
{
return false;
}
}
return true;
}
}
}