Skip to content

Commit 9985902

Browse files
committed
Add OpenAPI formatter for PS
1 parent cd77440 commit 9985902

File tree

6 files changed

+272
-52
lines changed

6 files changed

+272
-52
lines changed
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text.RegularExpressions;
5+
using Humanizer;
6+
using Humanizer.Inflections;
7+
using Microsoft.OpenApi.Models;
8+
using Microsoft.OpenApi.Services;
9+
10+
namespace Microsoft.OpenApi.Hidi.Formatters
11+
{
12+
internal class PowerShellFormatter : OpenApiVisitorBase
13+
{
14+
private const string DefaultPutPrefix = ".Update";
15+
private const string PowerShellPutPrefix = ".Set";
16+
private readonly Stack<OpenApiSchema> _schemaLoop = new();
17+
private static readonly Regex s_oDataCastRegex = new("(.*(?<=[a-z]))\\.(As(?=[A-Z]).*)", RegexOptions.Compiled, TimeSpan.FromSeconds(5));
18+
private static readonly Regex s_hashSuffixRegex = new(@"^[^-]+", RegexOptions.Compiled, TimeSpan.FromSeconds(5));
19+
private static readonly Regex s_oDataRefRegex = new("(?<=[a-z])Ref(?=[A-Z])", RegexOptions.Compiled, TimeSpan.FromSeconds(5));
20+
21+
static PowerShellFormatter()
22+
{
23+
// Add singularization exclusions.
24+
// TODO: Read exclusions from a user provided file.
25+
Vocabularies.Default.AddSingular("(drive)s$", "$1"); // drives does not properly singularize to drive.
26+
Vocabularies.Default.AddSingular("(data)$", "$1"); // exclude the following from singularization.
27+
Vocabularies.Default.AddSingular("(delta)$", "$1");
28+
Vocabularies.Default.AddSingular("(quota)$", "$1");
29+
Vocabularies.Default.AddSingular("(statistics)$", "$1");
30+
}
31+
32+
//TODO: FHL for PS
33+
// Fixes (Order matters):
34+
// 1. Singularize operationId operationIdSegments.
35+
// 2. Add '_' to verb in an operationId.
36+
// 3. Fix odata cast operationIds.
37+
// 4. Fix hash suffix in operationIds.
38+
// 5. Fix Put operation id should have -> {xxx}_Set{Yyy}
39+
// 5. Fix anyOf and oneOf schema.
40+
// 6. Add AdditionalProperties to object schemas.
41+
42+
public override void Visit(OpenApiSchema schema)
43+
{
44+
AddAddtionalPropertiesToSchema(schema);
45+
ResolveAnyOfSchema(schema);
46+
ResolveOneOfSchema(schema);
47+
48+
base.Visit(schema);
49+
}
50+
51+
public override void Visit(OpenApiPathItem pathItem)
52+
{
53+
if (pathItem.Operations.ContainsKey(OperationType.Put))
54+
{
55+
var operationId = pathItem.Operations[OperationType.Put].OperationId;
56+
pathItem.Operations[OperationType.Put].OperationId = ResolvePutOperationId(operationId);
57+
}
58+
59+
base.Visit(pathItem);
60+
}
61+
62+
public override void Visit(OpenApiOperation operation)
63+
{
64+
if (operation.OperationId == null)
65+
throw new ArgumentNullException(nameof(operation.OperationId), $"OperationId is required {PathString}");
66+
67+
var operationId = operation.OperationId;
68+
69+
operationId = RemoveHashSuffix(operationId);
70+
operationId = ResolveODataCastOperationId(operationId);
71+
operationId = ResolveByRefOperationId(operationId);
72+
73+
74+
var operationIdSegments = operationId.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries).ToList();
75+
operationId = SingularizeAndDeduplicateOperationId(operationIdSegments);
76+
77+
operation.OperationId = operationId;
78+
base.Visit(operation);
79+
}
80+
81+
private void AddAddtionalPropertiesToSchema(OpenApiSchema schema)
82+
{
83+
if (schema != null && !_schemaLoop.Contains(schema) && "object".Equals(schema?.Type, StringComparison.OrdinalIgnoreCase))
84+
{
85+
schema.AdditionalProperties = new OpenApiSchema() { Type = "object" };
86+
87+
/* Because 'additionalProperties' are now being walked,
88+
* we need a way to keep track of visited schemas to avoid
89+
* endlessly creating and walking them in an infinite recursion.
90+
*/
91+
_schemaLoop.Push(schema.AdditionalProperties);
92+
}
93+
}
94+
95+
private static void ResolveOneOfSchema(OpenApiSchema schema)
96+
{
97+
if (schema.OneOf?.Any() ?? false)
98+
{
99+
var newSchema = schema.OneOf.FirstOrDefault();
100+
schema.OneOf = null;
101+
FlattenSchema(schema, newSchema);
102+
}
103+
}
104+
105+
private static void ResolveAnyOfSchema(OpenApiSchema schema)
106+
{
107+
if (schema.AnyOf?.Any() ?? false)
108+
{
109+
var newSchema = schema.AnyOf.FirstOrDefault();
110+
schema.AnyOf = null;
111+
FlattenSchema(schema, newSchema);
112+
}
113+
}
114+
115+
private static string ResolvePutOperationId(string operationId)
116+
{
117+
return operationId.Contains(DefaultPutPrefix) ?
118+
operationId.Replace(DefaultPutPrefix, PowerShellPutPrefix) : operationId;
119+
}
120+
121+
private static string ResolveByRefOperationId(string operationId)
122+
{
123+
// Update $ref path operationId name
124+
// Ref key word is enclosed between lower-cased and upper-cased letters
125+
// Ex.: applications_GetRefCreatedOnBehalfOf to applications_GetCreatedOnBehalfOfByRef
126+
return s_oDataRefRegex.Match(operationId).Success ? $"{s_oDataRefRegex.Replace(operationId, string.Empty)}ByRef" : operationId;
127+
}
128+
129+
private static string ResolveODataCastOperationId(string operationId)
130+
{
131+
var match = s_oDataCastRegex.Match(operationId);
132+
return match.Success ? $"{match.Groups[1]}{match.Groups[2]}" : operationId;
133+
}
134+
135+
private static string SingularizeAndDeduplicateOperationId(IList<string> operationIdSegments)
136+
{
137+
var segmentsCount = operationIdSegments.Count;
138+
var lastSegmentIndex = segmentsCount - 1;
139+
var singularizedSegments = new List<string>();
140+
141+
for (int x = 0; x < segmentsCount; x++)
142+
{
143+
var segment = operationIdSegments[x].Singularize(inputIsKnownToBePlural: false);
144+
145+
// If a segment name is contained in the previous segment, the latter is considered a duplicate.
146+
// The last segment is ignored as a rule.
147+
if ((x > 0 && x < lastSegmentIndex) && singularizedSegments.Last().Equals(segment, StringComparison.OrdinalIgnoreCase))
148+
continue;
149+
150+
singularizedSegments.Add(segment);
151+
}
152+
return string.Join(".", singularizedSegments);
153+
}
154+
155+
private static string RemoveHashSuffix(string operationId)
156+
{
157+
// Remove hash suffix values from OperationIds.
158+
return s_hashSuffixRegex.Match(operationId).Value;
159+
}
160+
161+
private static void FlattenSchema(OpenApiSchema schema, OpenApiSchema newSchema)
162+
{
163+
if (newSchema != null)
164+
{
165+
if (newSchema.Reference != null)
166+
{
167+
schema.Reference = newSchema.Reference;
168+
schema.UnresolvedReference = true;
169+
}
170+
else
171+
{
172+
// Copies schema properties based on https://github.com/microsoft/OpenAPI.NET.OData/pull/264.
173+
CopySchema(schema, newSchema);
174+
}
175+
}
176+
}
177+
178+
private static void CopySchema(OpenApiSchema schema, OpenApiSchema newSchema)
179+
{
180+
schema.Title ??= newSchema.Title;
181+
schema.Type ??= newSchema.Type;
182+
schema.Format ??= newSchema.Format;
183+
schema.Description ??= newSchema.Description;
184+
schema.Maximum ??= newSchema.Maximum;
185+
schema.ExclusiveMaximum ??= newSchema.ExclusiveMaximum;
186+
schema.Minimum ??= newSchema.Minimum;
187+
schema.ExclusiveMinimum ??= newSchema.ExclusiveMinimum;
188+
schema.MaxLength ??= newSchema.MaxLength;
189+
schema.MinLength ??= newSchema.MinLength;
190+
schema.Pattern ??= newSchema.Pattern;
191+
schema.MultipleOf ??= newSchema.MultipleOf;
192+
schema.Not ??= newSchema.Not;
193+
schema.Required ??= newSchema.Required;
194+
schema.Items ??= newSchema.Items;
195+
schema.MaxItems ??= newSchema.MaxItems;
196+
schema.MinItems ??= newSchema.MinItems;
197+
schema.UniqueItems ??= newSchema.UniqueItems;
198+
schema.Properties ??= newSchema.Properties;
199+
schema.MaxProperties ??= newSchema.MaxProperties;
200+
schema.MinProperties ??= newSchema.MinProperties;
201+
schema.Discriminator ??= newSchema.Discriminator;
202+
schema.ExternalDocs ??= newSchema.ExternalDocs;
203+
schema.Enum ??= newSchema.Enum;
204+
schema.ReadOnly = !schema.ReadOnly ? newSchema.ReadOnly : schema.ReadOnly;
205+
schema.WriteOnly = !schema.WriteOnly ? newSchema.WriteOnly : schema.WriteOnly;
206+
schema.Nullable = !schema.Nullable ? newSchema.Nullable : schema.Nullable;
207+
schema.Deprecated = !schema.Deprecated ? newSchema.Deprecated : schema.Deprecated;
208+
}
209+
}
210+
}

src/Microsoft.OpenApi.Hidi/Handlers/TransformCommandHandler.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ internal class TransformCommandHandler : ICommandHandler
2929
public Option<string> FilterByCollectionOption { get; set; }
3030
public Option<bool> InlineLocalOption { get; set; }
3131
public Option<bool> InlineExternalOption { get; set; }
32+
public Option<string?> LanguageFormatOption { get; set; }
3233

3334
public int Invoke(InvocationContext context)
3435
{
@@ -49,6 +50,7 @@ public async Task<int> InvokeAsync(InvocationContext context)
4950
LogLevel logLevel = context.ParseResult.GetValueForOption(LogLevelOption);
5051
bool inlineLocal = context.ParseResult.GetValueForOption(InlineLocalOption);
5152
bool inlineExternal = context.ParseResult.GetValueForOption(InlineExternalOption);
53+
string? languageFormatOption = context.ParseResult.GetValueForOption(LanguageFormatOption);
5254
string filterbyoperationids = context.ParseResult.GetValueForOption(FilterByOperationIdsOption);
5355
string filterbytags = context.ParseResult.GetValueForOption(FilterByTagsOption);
5456
string filterbycollection = context.ParseResult.GetValueForOption(FilterByCollectionOption);
@@ -59,7 +61,7 @@ public async Task<int> InvokeAsync(InvocationContext context)
5961
var logger = loggerFactory.CreateLogger<OpenApiService>();
6062
try
6163
{
62-
await OpenApiService.TransformOpenApiDocument(openapi, csdl, csdlFilter, output, cleanOutput, version, metadataVersion, format, terseOutput, settingsFile, inlineLocal, inlineExternal, filterbyoperationids, filterbytags, filterbycollection, logger, cancellationToken);
64+
await OpenApiService.TransformOpenApiDocument(openapi, csdl, csdlFilter, output, cleanOutput, version, metadataVersion, format, terseOutput, settingsFile, inlineLocal, inlineExternal, languageFormatOption, filterbyoperationids, filterbytags, filterbycollection, logger, cancellationToken);
6365

6466
return 0;
6567
}

src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
</ItemGroup>
3838

3939
<ItemGroup>
40+
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
4041
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
4142
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
4243
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />

0 commit comments

Comments
 (0)