Skip to content

Commit abbd67b

Browse files
Support customizing filename in M.E.ApiDescription.Server tool (#56974)
* Add option to specify custom OpenAPI output file name * Changed behavior to replace project name instead like recommended * Updated dotnet-getdocument options to show GetDocument.Insider commands in it with help command * Update command name, add file name validation and add tests * Update missing command description and remove extra options in --help command of dotnet-document * Fix tests
1 parent a97352c commit abbd67b

File tree

5 files changed

+146
-36
lines changed

5 files changed

+146
-36
lines changed

src/Tools/GetDocumentInsider/src/Commands/GetDocumentCommand.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ internal sealed class GetDocumentCommand : ProjectCommandBase
1919
private CommandOption _output;
2020
private CommandOption _openApiVersion;
2121
private CommandOption _documentName;
22+
private CommandOption _fileName;
2223

2324
public GetDocumentCommand(IConsole console) : base(console)
2425
{
@@ -32,6 +33,7 @@ public override void Configure(CommandLineApplication command)
3233
_output = command.Option("--output <Directory>", Resources.OutputDescription);
3334
_openApiVersion = command.Option("--openapi-version <Version>", Resources.OpenApiVersionDescription);
3435
_documentName = command.Option("--document-name <Name>", Resources.DocumentNameDescription);
36+
_fileName = command.Option("--file-name <Name>", Resources.FileNameDescription);
3537
}
3638

3739
protected override void Validate()
@@ -142,6 +144,7 @@ protected override int Execute()
142144
DocumentName = _documentName.Value(),
143145
ProjectName = ProjectName.Value(),
144146
Reporter = Reporter,
147+
FileName = _fileName.Value()
145148
};
146149

147150
return new GetDocumentCommandWorker(context).Process();

src/Tools/GetDocumentInsider/src/Commands/GetDocumentCommandContext.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,13 @@ public class GetDocumentCommandContext
3030
// Generates all documents if not provided.
3131
public string DocumentName { get; set; }
3232

33+
/// <summary>
34+
/// The name of the generated OpenAPI document.
35+
/// When the default document (v1) is generated, this maps
36+
/// to {FileName}.json. For custom file names, this maps to
37+
/// {FileName}_{DocumentName}.json.
38+
/// </summary>
39+
public string FileName { get; set; }
40+
3341
public IReporter Reporter { get; set; }
3442
}

src/Tools/GetDocumentInsider/src/Commands/GetDocumentCommandWorker.cs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5-
using System.Collections;
65
using System.Collections.Generic;
76
using System.IO;
87
using System.Linq;
98
using System.Reflection;
109
using System.Text;
10+
using System.Text.RegularExpressions;
1111
using System.Threading;
1212
using System.Threading.Tasks;
1313
using Microsoft.Extensions.Hosting;
@@ -267,6 +267,12 @@ private bool GetDocuments(IServiceProvider services)
267267
return false;
268268
}
269269

270+
if (!string.IsNullOrWhiteSpace(_context.FileName) && !Regex.IsMatch(_context.FileName, "^([A-Za-z0-9-_]+)$"))
271+
{
272+
_reporter.WriteError(Resources.FileNameFormatInvalid);
273+
return false;
274+
}
275+
270276
// Write out the documents.
271277
var found = false;
272278
Directory.CreateDirectory(_context.OutputDirectory);
@@ -279,7 +285,8 @@ private bool GetDocuments(IServiceProvider services)
279285
_context.OutputDirectory,
280286
generateMethod,
281287
service,
282-
generateWithVersionMethod);
288+
generateWithVersionMethod,
289+
_context.FileName);
283290
if (filePath == null)
284291
{
285292
return false;
@@ -308,7 +315,8 @@ private string GetDocument(
308315
string outputDirectory,
309316
MethodInfo generateMethod,
310317
object service,
311-
MethodInfo? generateWithVersionMethod)
318+
MethodInfo? generateWithVersionMethod,
319+
string fileName)
312320
{
313321
_reporter.WriteInformation(Resources.FormatGeneratingDocument(documentName));
314322

@@ -355,7 +363,9 @@ private string GetDocument(
355363
return null;
356364
}
357365

358-
var filePath = GetDocumentPath(documentName, projectName, outputDirectory);
366+
fileName = !string.IsNullOrWhiteSpace(fileName) ? fileName : projectName;
367+
368+
var filePath = GetDocumentPath(documentName, fileName, outputDirectory);
359369
_reporter.WriteInformation(Resources.FormatWritingDocument(documentName, filePath));
360370
try
361371
{
@@ -374,13 +384,14 @@ private string GetDocument(
374384
return filePath;
375385
}
376386

377-
private static string GetDocumentPath(string documentName, string projectName, string outputDirectory)
387+
private static string GetDocumentPath(string documentName, string fileName, string outputDirectory)
378388
{
379389
string path;
390+
380391
if (string.Equals(DefaultDocumentName, documentName, StringComparison.Ordinal))
381392
{
382393
// Leave default document name out of the filename.
383-
path = projectName + JsonExtension;
394+
path = fileName + JsonExtension;
384395
}
385396
else
386397
{
@@ -395,7 +406,7 @@ private static string GetDocumentPath(string documentName, string projectName, s
395406
sanitizedDocumentName = sanitizedDocumentName.Replace(InvalidFilenameString, DotString);
396407
}
397408

398-
path = $"{projectName}_{documentName}{JsonExtension}";
409+
path = $"{fileName}_{documentName}{JsonExtension}";
399410
}
400411

401412
if (!string.IsNullOrEmpty(outputDirectory))

src/Tools/GetDocumentInsider/src/Resources.resx

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
<?xml version="1.0" encoding="utf-8"?>
1+
<?xml version="1.0" encoding="utf-8"?>
22
<root>
3-
<!--
4-
Microsoft ResX Schema
5-
3+
<!--
4+
Microsoft ResX Schema
5+
66
Version 2.0
7-
8-
The primary goals of this format is to allow a simple XML format
9-
that is mostly human readable. The generation and parsing of the
10-
various data types are done through the TypeConverter classes
7+
8+
The primary goals of this format is to allow a simple XML format
9+
that is mostly human readable. The generation and parsing of the
10+
various data types are done through the TypeConverter classes
1111
associated with the data types.
12-
12+
1313
Example:
14-
14+
1515
... ado.net/XML headers & schema ...
1616
<resheader name="resmimetype">text/microsoft-resx</resheader>
1717
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
2626
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
2727
<comment>This is a comment</comment>
2828
</data>
29-
30-
There are any number of "resheader" rows that contain simple
29+
30+
There are any number of "resheader" rows that contain simple
3131
name/value pairs.
32-
33-
Each data row contains a name, and value. The row also contains a
34-
type or mimetype. Type corresponds to a .NET class that support
35-
text/value conversion through the TypeConverter architecture.
36-
Classes that don't support this are serialized and stored with the
32+
33+
Each data row contains a name, and value. The row also contains a
34+
type or mimetype. Type corresponds to a .NET class that support
35+
text/value conversion through the TypeConverter architecture.
36+
Classes that don't support this are serialized and stored with the
3737
mimetype set.
38-
39-
The mimetype is used for serialized objects, and tells the
40-
ResXResourceReader how to depersist the object. This is currently not
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
4141
extensible. For a given mimetype the value must be set accordingly:
42-
43-
Note - application/x-microsoft.net.object.binary.base64 is the format
44-
that the ResXResourceWriter will generate, however the reader can
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
4545
read any of the formats listed below.
46-
46+
4747
mimetype: application/x-microsoft.net.object.binary.base64
48-
value : The object must be serialized with
48+
value : The object must be serialized with
4949
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
5050
: and then encoded with base64 encoding.
51-
51+
5252
mimetype: application/x-microsoft.net.object.soap.base64
53-
value : The object must be serialized with
53+
value : The object must be serialized with
5454
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
5555
: and then encoded with base64 encoding.
5656
5757
mimetype: application/x-microsoft.net.object.bytearray.base64
58-
value : The object must be serialized into a byte array
58+
value : The object must be serialized into a byte array
5959
: using a System.ComponentModel.TypeConverter
6060
: and then encoded with base64 encoding.
6161
-->
@@ -209,4 +209,10 @@
209209
<data name="VersionedGenerateMethod" xml:space="preserve">
210210
<value>Using discovered `GenerateAsync` overload with version parameter.</value>
211211
</data>
212-
</root>
212+
<data name="FileNameDescription" xml:space="preserve">
213+
<value>The name of the generated OpenAPI document. When the default document (v1) is generated, this maps to {FileName}.json. For custom file names, this maps to {FileName}_{DocumentName}.json.</value>
214+
</data>
215+
<data name="FileNameFormatInvalid" xml:space="preserve">
216+
<value>FileName format invalid, only Alphanumeric and "_ -" authorized</value>
217+
</data>
218+
</root>

src/Tools/GetDocumentInsider/tests/GetDocumentTests.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,4 +144,86 @@ public void GetDocument_WithInvalidDocumentName_Errors()
144144
Assert.False(File.Exists(Path.Combine(outputPath.FullName, "Sample_internal.json")));
145145
Assert.False(File.Exists(Path.Combine(outputPath.FullName, "Sample_invalid.json")));
146146
}
147+
148+
[Theory]
149+
[InlineData("customFileName")]
150+
[InlineData("custom-File_Name")]
151+
[InlineData("custom_File-Name")]
152+
[InlineData("custom1File2Name")]
153+
public void GetDocument_WithValidFileName_Works(string fileName)
154+
{
155+
// Arrange
156+
var outputPath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()));
157+
var app = new Program(_console);
158+
159+
// Act
160+
app.Run([
161+
"--assembly", _testAppAssembly,
162+
"--project", _testAppProject,
163+
"--framework", _testAppFrameworkMoniker,
164+
"--tools-directory", _toolsDirectory,
165+
"--output", outputPath.FullName,
166+
"--file-list", Path.Combine(outputPath.FullName, "file-list.cache"),
167+
"--file-name", fileName
168+
], new GetDocumentCommand(_console), throwOnUnexpectedArg: false);
169+
170+
// Assert
171+
Assert.True(File.Exists(Path.Combine(outputPath.FullName, $"{fileName}.json")));
172+
Assert.True(File.Exists(Path.Combine(outputPath.FullName, $"{fileName}_internal.json")));
173+
Assert.False(File.Exists(Path.Combine(outputPath.FullName, "Sample.json")));
174+
Assert.False(File.Exists(Path.Combine(outputPath.FullName, "Sample_internal.json")));
175+
}
176+
177+
[Theory]
178+
[InlineData("customFile=ù^*Name")]
179+
[InlineData("&$*")]
180+
public void GetDocument_WithInvalideFileName_Errors(string fileName)
181+
{
182+
// Arrange
183+
var outputPath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()));
184+
var app = new Program(_console);
185+
186+
// Act
187+
app.Run([
188+
"--assembly", _testAppAssembly,
189+
"--project", _testAppProject,
190+
"--framework", _testAppFrameworkMoniker,
191+
"--tools-directory", _toolsDirectory,
192+
"--output", outputPath.FullName,
193+
"--file-list", Path.Combine(outputPath.FullName, "file-list.cache"),
194+
"--file-name", fileName
195+
], new GetDocumentCommand(_console), throwOnUnexpectedArg: false);
196+
197+
// Assert
198+
199+
Assert.Contains("FileName format invalid, only Alphanumeric and \"_ -\" authorized", _console.GetOutput());
200+
Assert.False(File.Exists(Path.Combine(outputPath.FullName, $"{fileName}.json")));
201+
Assert.False(File.Exists(Path.Combine(outputPath.FullName, "Sample.json")));
202+
Assert.False(File.Exists(Path.Combine(outputPath.FullName, "Sample_internal.json")));
203+
}
204+
205+
[Fact]
206+
public void GetDocument_WithEmptyFileName_Works()
207+
{
208+
// Arrange
209+
var outputPath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()));
210+
var app = new Program(_console);
211+
212+
// Act
213+
app.Run([
214+
"--assembly", _testAppAssembly,
215+
"--project", _testAppProject,
216+
"--framework", _testAppFrameworkMoniker,
217+
"--tools-directory", _toolsDirectory,
218+
"--output", outputPath.FullName,
219+
"--file-list", Path.Combine(outputPath.FullName, "file-list.cache"),
220+
"--file-name", ""
221+
], new GetDocumentCommand(_console), throwOnUnexpectedArg: false);
222+
223+
// Assert
224+
Assert.False(File.Exists(Path.Combine(outputPath.FullName, ".json")));
225+
Assert.False(File.Exists(Path.Combine(outputPath.FullName, "_internal.json")));
226+
Assert.True(File.Exists(Path.Combine(outputPath.FullName, "Sample.json")));
227+
Assert.True(File.Exists(Path.Combine(outputPath.FullName, "Sample_internal.json")));
228+
}
147229
}

0 commit comments

Comments
 (0)