@@ -19,10 +19,18 @@ public static class OpenApiFilterService
19
19
/// </summary>
20
20
/// <param name="operationIds">Comma delimited list of operationIds or * for all operations.</param>
21
21
/// <param name="tags">Comma delimited list of tags or a single regex.</param>
22
+ /// <param name="requestUrls">A dictionary of requests from a postman collection.</param>
23
+ /// <param name="source">The input OpenAPI document.</param>
22
24
/// <returns>A predicate.</returns>
23
- public static Func < OpenApiOperation , bool > CreatePredicate ( string operationIds = null , string tags = null )
25
+ public static Func < string , OperationType ? , OpenApiOperation , bool > CreatePredicate ( string operationIds = null ,
26
+ string tags = null , Dictionary < string , List < string > > requestUrls = null , OpenApiDocument source = null )
24
27
{
25
- Func < OpenApiOperation , bool > predicate ;
28
+ Func < string , OperationType ? , OpenApiOperation , bool > predicate ;
29
+
30
+ if ( requestUrls != null && ( operationIds != null || tags != null ) )
31
+ {
32
+ throw new InvalidOperationException ( "Cannot filter by Postman collection and either operationIds and tags at the same time." ) ;
33
+ }
26
34
if ( ! string . IsNullOrEmpty ( operationIds ) && ! string . IsNullOrEmpty ( tags ) )
27
35
{
28
36
throw new InvalidOperationException ( "Cannot specify both operationIds and tags at the same time." ) ;
@@ -31,12 +39,12 @@ public static Func<OpenApiOperation, bool> CreatePredicate(string operationIds =
31
39
{
32
40
if ( operationIds == "*" )
33
41
{
34
- predicate = ( o ) => true ; // All operations
42
+ predicate = ( url , operationType , operation ) => true ; // All operations
35
43
}
36
44
else
37
45
{
38
46
var operationIdsArray = operationIds . Split ( ',' ) ;
39
- predicate = ( o ) => operationIdsArray . Contains ( o . OperationId ) ;
47
+ predicate = ( url , operationType , operation ) => operationIdsArray . Contains ( operation . OperationId ) ;
40
48
}
41
49
}
42
50
else if ( tags != null )
@@ -46,16 +54,59 @@ public static Func<OpenApiOperation, bool> CreatePredicate(string operationIds =
46
54
{
47
55
var regex = new Regex ( tagsArray [ 0 ] ) ;
48
56
49
- predicate = ( o ) => o . Tags . Any ( t => regex . IsMatch ( t . Name ) ) ;
57
+ predicate = ( url , operationType , operation ) => operation . Tags . Any ( tag => regex . IsMatch ( tag . Name ) ) ;
50
58
}
51
59
else
52
60
{
53
- predicate = ( o ) => o . Tags . Any ( t => tagsArray . Contains ( t . Name ) ) ;
61
+ predicate = ( url , operationType , operation ) => operation . Tags . Any ( tag => tagsArray . Contains ( tag . Name ) ) ;
62
+ }
63
+ }
64
+ else if ( requestUrls != null )
65
+ {
66
+ var operationTypes = new List < string > ( ) ;
67
+
68
+ if ( source != null )
69
+ {
70
+ var apiVersion = source . Info . Version ;
71
+
72
+ var sources = new Dictionary < string , OpenApiDocument > { { apiVersion , source } } ;
73
+ var rootNode = CreateOpenApiUrlTreeNode ( sources ) ;
74
+
75
+ // Iterate through urls dictionary and fetch operations for each url
76
+ foreach ( var path in requestUrls )
77
+ {
78
+ var serverList = source . Servers ;
79
+ var url = FormatUrlString ( path . Key , serverList ) ;
80
+
81
+ var openApiOperations = GetOpenApiOperations ( rootNode , url , apiVersion ) ;
82
+ if ( openApiOperations == null )
83
+ {
84
+ continue ;
85
+ }
86
+
87
+ // Add the available ops if they are in the postman collection. See path.Value
88
+ foreach ( var ops in openApiOperations )
89
+ {
90
+ if ( path . Value . Contains ( ops . Key . ToString ( ) . ToUpper ( ) ) )
91
+ {
92
+ operationTypes . Add ( ops . Key + url ) ;
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ if ( ! operationTypes . Any ( ) )
99
+ {
100
+ throw new ArgumentException ( "The urls in the Postman collection supplied could not be found." ) ;
54
101
}
102
+
103
+ // predicate for matching url and operationTypes
104
+ predicate = ( path , operationType , operation ) => operationTypes . Contains ( operationType + path ) ;
55
105
}
106
+
56
107
else
57
108
{
58
- throw new InvalidOperationException ( "Either operationId(s) or tag(s) need to be specified." ) ;
109
+ throw new InvalidOperationException ( "Either operationId(s), tag(s) or Postman collection need to be specified." ) ;
59
110
}
60
111
61
112
return predicate ;
@@ -67,12 +118,12 @@ public static Func<OpenApiOperation, bool> CreatePredicate(string operationIds =
67
118
/// <param name="source">The target <see cref="OpenApiDocument"/>.</param>
68
119
/// <param name="predicate">A predicate function.</param>
69
120
/// <returns>A partial OpenAPI document.</returns>
70
- public static OpenApiDocument CreateFilteredDocument ( OpenApiDocument source , Func < OpenApiOperation , bool > predicate )
121
+ public static OpenApiDocument CreateFilteredDocument ( OpenApiDocument source , Func < string , OperationType ? , OpenApiOperation , bool > predicate )
71
122
{
72
123
// Fetch and copy title, graphVersion and server info from OpenApiDoc
73
124
var subset = new OpenApiDocument
74
125
{
75
- Info = new OpenApiInfo ( )
126
+ Info = new OpenApiInfo
76
127
{
77
128
Title = source . Info . Title + " - Subset" ,
78
129
Description = source . Info . Description ,
@@ -83,13 +134,11 @@ public static OpenApiDocument CreateFilteredDocument(OpenApiDocument source, Fun
83
134
Extensions = source . Info . Extensions
84
135
} ,
85
136
86
- Components = new OpenApiComponents ( )
137
+ Components = new OpenApiComponents { SecuritySchemes = source . Components . SecuritySchemes } ,
138
+ SecurityRequirements = source . SecurityRequirements ,
139
+ Servers = source . Servers
87
140
} ;
88
141
89
- subset . Components . SecuritySchemes = source . Components . SecuritySchemes ;
90
- subset . SecurityRequirements = source . SecurityRequirements ;
91
- subset . Servers = source . Servers ;
92
-
93
142
var results = FindOperations ( source , predicate ) ;
94
143
foreach ( var result in results )
95
144
{
@@ -111,7 +160,10 @@ public static OpenApiDocument CreateFilteredDocument(OpenApiDocument source, Fun
111
160
}
112
161
}
113
162
114
- pathItem . Operations . Add ( ( OperationType ) result . CurrentKeys . Operation , result . Operation ) ;
163
+ if ( result . CurrentKeys . Operation != null )
164
+ {
165
+ pathItem . Operations . Add ( ( OperationType ) result . CurrentKeys . Operation , result . Operation ) ;
166
+ }
115
167
}
116
168
117
169
if ( subset . Paths == null )
@@ -124,11 +176,103 @@ public static OpenApiDocument CreateFilteredDocument(OpenApiDocument source, Fun
124
176
return subset ;
125
177
}
126
178
127
- private static IList < SearchResult > FindOperations ( OpenApiDocument graphOpenApi , Func < OpenApiOperation , bool > predicate )
179
+ /// <summary>
180
+ /// Creates an <see cref="OpenApiUrlTreeNode"/> from a collection of <see cref="OpenApiDocument"/>.
181
+ /// </summary>
182
+ /// <param name="sources">Dictionary of labels and their corresponding <see cref="OpenApiDocument"/> objects.</param>
183
+ /// <returns>The created <see cref="OpenApiUrlTreeNode"/>.</returns>
184
+ public static OpenApiUrlTreeNode CreateOpenApiUrlTreeNode ( Dictionary < string , OpenApiDocument > sources )
185
+ {
186
+ var rootNode = OpenApiUrlTreeNode . Create ( ) ;
187
+ foreach ( var source in sources )
188
+ {
189
+ rootNode . Attach ( source . Value , source . Key ) ;
190
+ }
191
+ return rootNode ;
192
+ }
193
+
194
+ private static IDictionary < OperationType , OpenApiOperation > GetOpenApiOperations ( OpenApiUrlTreeNode rootNode , string relativeUrl , string label )
195
+ {
196
+ if ( relativeUrl . Equals ( "/" , StringComparison . Ordinal ) && rootNode . HasOperations ( label ) )
197
+ {
198
+ return rootNode . PathItems [ label ] . Operations ;
199
+ }
200
+
201
+ var urlSegments = relativeUrl . Split ( new [ ] { '/' } , StringSplitOptions . RemoveEmptyEntries ) ;
202
+
203
+ IDictionary < OperationType , OpenApiOperation > operations = null ;
204
+
205
+ var targetChild = rootNode ;
206
+
207
+ /* This will help keep track of whether we've skipped a segment
208
+ * in the target url due to a possible parameter naming mismatch
209
+ * with the corresponding OpenApiUrlTreeNode target child segment.
210
+ */
211
+ var parameterNameOffset = 0 ;
212
+
213
+ for ( var i = 0 ; i < urlSegments ? . Length ; i ++ )
214
+ {
215
+ var tempTargetChild = targetChild ? . Children ?
216
+ . FirstOrDefault ( x => x . Key . Equals ( urlSegments [ i ] ,
217
+ StringComparison . OrdinalIgnoreCase ) ) . Value ;
218
+
219
+ // Segment name mismatch
220
+ if ( tempTargetChild == null )
221
+ {
222
+ if ( i == 0 )
223
+ {
224
+ /* If no match and we are at the 1st segment of the relative url,
225
+ * exit; no need to continue matching subsequent segments.
226
+ */
227
+ break ;
228
+ }
229
+
230
+ /* Attempt to get the parameter segment from the children of the current node:
231
+ * We are assuming a failed match because of different parameter namings
232
+ * between the relative url segment and the corresponding OpenApiUrlTreeNode segment name
233
+ * ex.: matching '/users/12345/messages' with '/users/{user-id}/messages'
234
+ */
235
+ tempTargetChild = targetChild ? . Children ?
236
+ . FirstOrDefault ( x => x . Value . IsParameter ) . Value ;
237
+
238
+ /* If no parameter segment exists in the children of the
239
+ * current node or we've already skipped a parameter
240
+ * segment in the relative url from the last pass,
241
+ * then exit; there's no match.
242
+ */
243
+ if ( tempTargetChild == null || parameterNameOffset > 0 )
244
+ {
245
+ break ;
246
+ }
247
+
248
+ /* To help us know we've skipped a
249
+ * corresponding segment in the relative url.
250
+ */
251
+ parameterNameOffset ++ ;
252
+ }
253
+ else
254
+ {
255
+ parameterNameOffset = 0 ;
256
+ }
257
+
258
+ // Move to the next segment
259
+ targetChild = tempTargetChild ;
260
+
261
+ // We want the operations of the last segment of the path.
262
+ if ( i == urlSegments . Length - 1 && targetChild . HasOperations ( label ) )
263
+ {
264
+ operations = targetChild . PathItems [ label ] . Operations ;
265
+ }
266
+ }
267
+
268
+ return operations ;
269
+ }
270
+
271
+ private static IList < SearchResult > FindOperations ( OpenApiDocument sourceDocument , Func < string , OperationType ? , OpenApiOperation , bool > predicate )
128
272
{
129
273
var search = new OperationSearch ( predicate ) ;
130
274
var walker = new OpenApiWalker ( search ) ;
131
- walker . Walk ( graphOpenApi ) ;
275
+ walker . Walk ( sourceDocument ) ;
132
276
return search . SearchResults ;
133
277
}
134
278
@@ -177,5 +321,23 @@ private static bool AddReferences(OpenApiComponents newComponents, OpenApiCompon
177
321
}
178
322
return moreStuff ;
179
323
}
324
+
325
+ private static string FormatUrlString ( string url , IList < OpenApiServer > serverList )
326
+ {
327
+ var queryPath = string . Empty ;
328
+ foreach ( var server in serverList )
329
+ {
330
+ var serverUrl = server . Url . TrimEnd ( '/' ) ;
331
+ if ( ! url . Contains ( serverUrl ) )
332
+ {
333
+ continue ;
334
+ }
335
+
336
+ var querySegments = url . Split ( new [ ] { serverUrl } , StringSplitOptions . None ) ;
337
+ queryPath = querySegments [ 1 ] ;
338
+ }
339
+
340
+ return queryPath ;
341
+ }
180
342
}
181
343
}
0 commit comments