Skip to content

Commit 4a24112

Browse files
authored
Merge branch 'vnext' into mk/upgrade-libs
2 parents 8f272aa + 9d82233 commit 4a24112

29 files changed

+1497
-448
lines changed

.vscode/launch.json

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,36 @@
55
// Use IntelliSense to find out which attributes exist for C# debugging
66
// Use hover for the description of the existing attributes
77
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
8-
"name": ".NET Core Launch (console)",
8+
"name": "Launch Hidi",
99
"type": "coreclr",
1010
"request": "launch",
1111
"preLaunchTask": "build",
1212
// If you have changed target frameworks, make sure to update the program path.
1313
"program": "${workspaceFolder}/src/Microsoft.OpenApi.Hidi/bin/Debug/net7.0/Microsoft.OpenApi.Hidi.dll",
14-
"args": [],
14+
"args": ["plugin",
15+
"-m","C:\\Users\\darrmi\\src\\github\\microsoft\\openapi.net\\test\\Microsoft.OpenApi.Hidi.Tests\\UtilityFiles\\exampleapimanifest.json",
16+
"--of","./output"],
1517
"cwd": "${workspaceFolder}/src/Microsoft.OpenApi.Hidi",
1618
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
1719
"console": "internalConsole",
1820
"stopAtEntry": false
1921
},
22+
{
23+
// Use IntelliSense to find out which attributes exist for C# debugging
24+
// Use hover for the description of the existing attributes
25+
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
26+
"name": "Launch Workbench",
27+
"type": "coreclr",
28+
"request": "launch",
29+
"preLaunchTask": "build",
30+
// If you have changed target frameworks, make sure to update the program path.
31+
"program": "${workspaceFolder}/src/Microsoft.OpenApi.WorkBench/bin/Debug/net7.0-windows/Microsoft.OpenApi.Workbench.exe",
32+
"args": [],
33+
"cwd": "${workspaceFolder}/src/Microsoft.OpenApi.Workbench",
34+
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
35+
"console": "internalConsole",
36+
"stopAtEntry": false
37+
},
2038
{
2139
"name": ".NET Core Attach",
2240
"type": "coreclr",
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.CommandLine;
6+
7+
namespace Microsoft.OpenApi.Hidi.Extensions
8+
{
9+
internal static class CommandExtensions
10+
{
11+
public static void AddOptions(this Command command, IReadOnlyList<Option> options)
12+
{
13+
foreach (var option in options)
14+
{
15+
command.AddOption(option);
16+
}
17+
}
18+
}
19+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using Microsoft.OpenApi.Any;
2+
using Microsoft.OpenApi.Interfaces;
3+
using System.Collections.Generic;
4+
5+
namespace Microsoft.OpenApi.Hidi.Extensions
6+
{
7+
internal static class OpenApiExtensibleExtensions
8+
{
9+
/// <summary>
10+
/// Gets an extension value from the extensions dictionary.
11+
/// </summary>
12+
/// <param name="extensions">A dictionary of <see cref="IOpenApiExtension"/>.</param>
13+
/// <param name="extensionKey">The key corresponding to the <see cref="IOpenApiExtension"/>.</param>
14+
/// <returns>A <see cref="string"/> value matching the provided extensionKey. Return null when extensionKey is not found. </returns>
15+
public static string GetExtension(this IDictionary<string, IOpenApiExtension> extensions, string extensionKey)
16+
{
17+
if (extensions.TryGetValue(extensionKey, out var value) && value is OpenApiString castValue)
18+
{
19+
return castValue.Value;
20+
}
21+
return default;
22+
}
23+
}
24+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace Microsoft.OpenApi.Hidi.Extensions
6+
{
7+
/// <summary>
8+
/// Extension class for <see cref="string"/>.
9+
/// </summary>
10+
internal static class StringExtensions
11+
{
12+
/// <summary>
13+
/// Checks if the specified searchValue is equal to the target string based on the specified <see cref="StringComparison"/>.
14+
/// </summary>
15+
/// <param name="target">The target string to commpare to.</param>
16+
/// <param name="searchValue">The search string to seek.</param>
17+
/// <param name="comparison">The <see cref="StringComparison"/> to use. This defaults to <see cref="StringComparison.OrdinalIgnoreCase"/>.</param>
18+
/// <returns>true if the searchValue parameter occurs within this string; otherwise, false.</returns>
19+
public static bool IsEquals(this string target, string searchValue, StringComparison comparison = StringComparison.OrdinalIgnoreCase)
20+
{
21+
if (string.IsNullOrWhiteSpace(target) || string.IsNullOrWhiteSpace(searchValue))
22+
{
23+
return false;
24+
}
25+
return target.Equals(searchValue, comparison);
26+
}
27+
28+
/// <summary>
29+
/// Splits the target string in substrings based on the specified char separator.
30+
/// </summary>
31+
/// <param name="target">The target string to split by char. </param>
32+
/// <param name="separator">The char separator.</param>
33+
/// <returns>An <see cref="IList{String}"/> containing substrings.</returns>
34+
public static IList<string> SplitByChar(this string target, char separator)
35+
{
36+
if (string.IsNullOrWhiteSpace(target))
37+
{
38+
return new List<string>();
39+
}
40+
return target.Split(new char[] { separator }, StringSplitOptions.RemoveEmptyEntries).ToList();
41+
}
42+
}
43+
}
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Text.RegularExpressions;
6+
using Humanizer;
7+
using Humanizer.Inflections;
8+
using Microsoft.OpenApi.Hidi.Extensions;
9+
using Microsoft.OpenApi.Models;
10+
using Microsoft.OpenApi.Services;
11+
12+
namespace Microsoft.OpenApi.Hidi.Formatters
13+
{
14+
internal class PowerShellFormatter : OpenApiVisitorBase
15+
{
16+
private const string DefaultPutPrefix = ".Update";
17+
private const string PowerShellPutPrefix = ".Set";
18+
private readonly Stack<OpenApiSchema> _schemaLoop = new();
19+
private static readonly Regex s_oDataCastRegex = new("(.*(?<=[a-z]))\\.(As(?=[A-Z]).*)", RegexOptions.Compiled, TimeSpan.FromSeconds(5));
20+
private static readonly Regex s_hashSuffixRegex = new(@"^[^-]+", RegexOptions.Compiled, TimeSpan.FromSeconds(5));
21+
private static readonly Regex s_oDataRefRegex = new("(?<=[a-z])Ref(?=[A-Z])", RegexOptions.Compiled, TimeSpan.FromSeconds(5));
22+
23+
static PowerShellFormatter()
24+
{
25+
// Add singularization exclusions.
26+
// Enhancement: Read exclusions from a user provided file.
27+
Vocabularies.Default.AddSingular("(drive)s$", "$1"); // drives does not properly singularize to drive.
28+
Vocabularies.Default.AddSingular("(data)$", "$1"); // exclude the following from singularization.
29+
Vocabularies.Default.AddSingular("(delta)$", "$1");
30+
Vocabularies.Default.AddSingular("(quota)$", "$1");
31+
Vocabularies.Default.AddSingular("(statistics)$", "$1");
32+
}
33+
34+
//FHL task for PS
35+
// Fixes (Order matters):
36+
// 1. Singularize operationId operationIdSegments.
37+
// 2. Add '_' to verb in an operationId.
38+
// 3. Fix odata cast operationIds.
39+
// 4. Fix hash suffix in operationIds.
40+
// 5. Fix Put operation id should have -> {xxx}_Set{Yyy}
41+
// 5. Fix anyOf and oneOf schema.
42+
// 6. Add AdditionalProperties to object schemas.
43+
44+
public override void Visit(OpenApiSchema schema)
45+
{
46+
AddAdditionalPropertiesToSchema(schema);
47+
ResolveAnyOfSchema(schema);
48+
ResolveOneOfSchema(schema);
49+
50+
base.Visit(schema);
51+
}
52+
53+
public override void Visit(OpenApiPathItem pathItem)
54+
{
55+
if (pathItem.Operations.TryGetValue(OperationType.Put, out var value))
56+
{
57+
var operationId = value.OperationId;
58+
pathItem.Operations[OperationType.Put].OperationId = ResolvePutOperationId(operationId);
59+
}
60+
61+
base.Visit(pathItem);
62+
}
63+
64+
public override void Visit(OpenApiOperation operation)
65+
{
66+
if (string.IsNullOrWhiteSpace(operation.OperationId))
67+
throw new ArgumentException($"OperationId is required {PathString}", nameof(operation));
68+
69+
var operationId = operation.OperationId;
70+
var operationTypeExtension = operation.Extensions.GetExtension("x-ms-docs-operation-type");
71+
if (operationTypeExtension.IsEquals("function"))
72+
operation.Parameters = ResolveFunctionParameters(operation.Parameters);
73+
74+
// Order matters. Resolve operationId.
75+
operationId = RemoveHashSuffix(operationId);
76+
if (operationTypeExtension.IsEquals("action") || operationTypeExtension.IsEquals("function"))
77+
operationId = RemoveKeyTypeSegment(operationId, operation.Parameters);
78+
operationId = SingularizeAndDeduplicateOperationId(operationId.SplitByChar('.'));
79+
operationId = ResolveODataCastOperationId(operationId);
80+
operationId = ResolveByRefOperationId(operationId);
81+
// Verb segment resolution should always be last. user.get -> user_Get
82+
operationId = ResolveVerbSegmentInOpertationId(operationId);
83+
84+
operation.OperationId = operationId;
85+
base.Visit(operation);
86+
}
87+
88+
private static string ResolveVerbSegmentInOpertationId(string operationId)
89+
{
90+
var charPos = operationId.LastIndexOf('.', operationId.Length - 1);
91+
if (operationId.Contains('_') || charPos < 0)
92+
return operationId;
93+
var newOperationId = new StringBuilder(operationId);
94+
newOperationId[charPos] = '_';
95+
operationId = newOperationId.ToString();
96+
return operationId;
97+
}
98+
99+
private static string ResolvePutOperationId(string operationId)
100+
{
101+
return operationId.Contains(DefaultPutPrefix, StringComparison.OrdinalIgnoreCase) ?
102+
operationId.Replace(DefaultPutPrefix, PowerShellPutPrefix) : operationId;
103+
}
104+
105+
private static string ResolveByRefOperationId(string operationId)
106+
{
107+
// Update $ref path operationId name
108+
// Ref key word is enclosed between lower-cased and upper-cased letters
109+
// Ex.: applications_GetRefCreatedOnBehalfOf to applications_GetCreatedOnBehalfOfByRef
110+
return s_oDataRefRegex.Match(operationId).Success ? $"{s_oDataRefRegex.Replace(operationId, string.Empty)}ByRef" : operationId;
111+
}
112+
113+
private static string ResolveODataCastOperationId(string operationId)
114+
{
115+
var match = s_oDataCastRegex.Match(operationId);
116+
return match.Success ? $"{match.Groups[1]}{match.Groups[2]}" : operationId;
117+
}
118+
119+
private static string SingularizeAndDeduplicateOperationId(IList<string> operationIdSegments)
120+
{
121+
var segmentsCount = operationIdSegments.Count;
122+
var lastSegmentIndex = segmentsCount - 1;
123+
var singularizedSegments = new List<string>();
124+
125+
for (int x = 0; x < segmentsCount; x++)
126+
{
127+
var segment = operationIdSegments[x].Singularize(inputIsKnownToBePlural: false);
128+
129+
// If a segment name is contained in the previous segment, the latter is considered a duplicate.
130+
// The last segment is ignored as a rule.
131+
if ((x > 0 && x < lastSegmentIndex) && singularizedSegments[singularizedSegments.Count - 1].Equals(segment, StringComparison.OrdinalIgnoreCase))
132+
continue;
133+
134+
singularizedSegments.Add(segment);
135+
}
136+
return string.Join(".", singularizedSegments);
137+
}
138+
139+
private static string RemoveHashSuffix(string operationId)
140+
{
141+
// Remove hash suffix values from OperationIds.
142+
return s_hashSuffixRegex.Match(operationId).Value;
143+
}
144+
145+
private static string RemoveKeyTypeSegment(string operationId, IList<OpenApiParameter> parameters)
146+
{
147+
var segments = operationId.SplitByChar('.');
148+
foreach (var parameter in parameters)
149+
{
150+
var keyTypeExtension = parameter.Extensions.GetExtension("x-ms-docs-key-type");
151+
if (keyTypeExtension != null && operationId.Contains(keyTypeExtension, StringComparison.OrdinalIgnoreCase))
152+
{
153+
segments.Remove(keyTypeExtension);
154+
}
155+
}
156+
return string.Join(".", segments);
157+
}
158+
159+
private static IList<OpenApiParameter> ResolveFunctionParameters(IList<OpenApiParameter> parameters)
160+
{
161+
foreach (var parameter in parameters.Where(static p => p.Content?.Any() ?? false))
162+
{
163+
// Replace content with a schema object of type array
164+
// for structured or collection-valued function parameters
165+
parameter.Content = null;
166+
parameter.Schema = new OpenApiSchema
167+
{
168+
Type = "array",
169+
Items = new OpenApiSchema
170+
{
171+
Type = "string"
172+
}
173+
};
174+
}
175+
return parameters;
176+
}
177+
178+
private void AddAdditionalPropertiesToSchema(OpenApiSchema schema)
179+
{
180+
if (schema != null && !_schemaLoop.Contains(schema) && "object".Equals(schema.Type, StringComparison.OrdinalIgnoreCase))
181+
{
182+
schema.AdditionalProperties = new OpenApiSchema() { Type = "object" };
183+
184+
/* Because 'additionalProperties' are now being walked,
185+
* we need a way to keep track of visited schemas to avoid
186+
* endlessly creating and walking them in an infinite recursion.
187+
*/
188+
_schemaLoop.Push(schema.AdditionalProperties);
189+
}
190+
}
191+
192+
private static void ResolveOneOfSchema(OpenApiSchema schema)
193+
{
194+
if (schema.OneOf?.Any() ?? false)
195+
{
196+
var newSchema = schema.OneOf.FirstOrDefault();
197+
schema.OneOf = null;
198+
FlattenSchema(schema, newSchema);
199+
}
200+
}
201+
202+
private static void ResolveAnyOfSchema(OpenApiSchema schema)
203+
{
204+
if (schema.AnyOf?.Any() ?? false)
205+
{
206+
var newSchema = schema.AnyOf.FirstOrDefault();
207+
schema.AnyOf = null;
208+
FlattenSchema(schema, newSchema);
209+
}
210+
}
211+
212+
private static void FlattenSchema(OpenApiSchema schema, OpenApiSchema newSchema)
213+
{
214+
if (newSchema != null)
215+
{
216+
if (newSchema.Reference != null)
217+
{
218+
schema.Reference = newSchema.Reference;
219+
schema.UnresolvedReference = true;
220+
}
221+
else
222+
{
223+
// Copies schema properties based on https://github.com/microsoft/OpenAPI.NET.OData/pull/264.
224+
CopySchema(schema, newSchema);
225+
}
226+
}
227+
}
228+
229+
private static void CopySchema(OpenApiSchema schema, OpenApiSchema newSchema)
230+
{
231+
schema.Title ??= newSchema.Title;
232+
schema.Type ??= newSchema.Type;
233+
schema.Format ??= newSchema.Format;
234+
schema.Description ??= newSchema.Description;
235+
schema.Maximum ??= newSchema.Maximum;
236+
schema.ExclusiveMaximum ??= newSchema.ExclusiveMaximum;
237+
schema.Minimum ??= newSchema.Minimum;
238+
schema.ExclusiveMinimum ??= newSchema.ExclusiveMinimum;
239+
schema.MaxLength ??= newSchema.MaxLength;
240+
schema.MinLength ??= newSchema.MinLength;
241+
schema.Pattern ??= newSchema.Pattern;
242+
schema.MultipleOf ??= newSchema.MultipleOf;
243+
schema.Not ??= newSchema.Not;
244+
schema.Required ??= newSchema.Required;
245+
schema.Items ??= newSchema.Items;
246+
schema.MaxItems ??= newSchema.MaxItems;
247+
schema.MinItems ??= newSchema.MinItems;
248+
schema.UniqueItems ??= newSchema.UniqueItems;
249+
schema.Properties ??= newSchema.Properties;
250+
schema.MaxProperties ??= newSchema.MaxProperties;
251+
schema.MinProperties ??= newSchema.MinProperties;
252+
schema.Discriminator ??= newSchema.Discriminator;
253+
schema.ExternalDocs ??= newSchema.ExternalDocs;
254+
schema.Enum ??= newSchema.Enum;
255+
schema.ReadOnly = !schema.ReadOnly ? newSchema.ReadOnly : schema.ReadOnly;
256+
schema.WriteOnly = !schema.WriteOnly ? newSchema.WriteOnly : schema.WriteOnly;
257+
schema.Nullable = !schema.Nullable ? newSchema.Nullable : schema.Nullable;
258+
schema.Deprecated = !schema.Deprecated ? newSchema.Deprecated : schema.Deprecated;
259+
}
260+
}
261+
}

0 commit comments

Comments
 (0)