Skip to content

Commit 8292767

Browse files
chrisradekChristopher Radek
authored andcommitted
[azsdk cli] add tsp init tool (#11628)
* [azsdk cli] add tsp init tool * address some pr feedback * better paths for tests * update processHelper and npxHelper to be async * add descriptions to parameters * use enum for templates in CLI path * little extra try/catch for what should be impossible case * template enum should be scoped to TypeSpecTool * add serviceNamespace fast validation * break out tsp tools into separate classes * async renaming * update tsp description * fix newlines * get rid of manual command routing * update tsp convert-swagger descriptions --------- Co-authored-by: Christopher Radek <Christopher.Radek@microsoft.com>
1 parent b7d9359 commit 8292767

6 files changed

Lines changed: 366 additions & 67 deletions

File tree

tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Tools/TspToolTests.cs renamed to tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Tools/TypeSpec/TspConvertToolTest.cs

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,24 @@
77

88
namespace Azure.Sdk.Tools.Cli.Tests.Tools
99
{
10-
public class TypeSpecToolTests
10+
public class TspConvertToolTests
1111
{
1212
[Test]
13-
public void GetCommand_ShouldReturnCommandWithSubcommands()
13+
public void GetCommand_ShouldReturnCommand()
1414
{
1515
// Arrange
1616
var npxHelper = new Mock<INpxHelper>().Object;
17-
var logger = new Mock<ILogger<TypeSpecTool>>().Object;
17+
var logger = new Mock<ILogger<TypeSpecConvertTool>>().Object;
1818
var outputService = new Mock<IOutputHelper>().Object;
19-
var tool = new TypeSpecTool(npxHelper, logger, outputService);
19+
var tool = new TypeSpecConvertTool(npxHelper, logger, outputService);
2020

2121
// Act
2222
var command = tool.GetCommand();
2323

2424
Assert.Multiple(() =>
2525
{
26-
Assert.That(command.Name, Is.EqualTo("tsp"));
27-
Assert.That(command.Description, Does.Contain("Tools for initializing TypeSpec projects"));
28-
Assert.That(command.Subcommands, Has.Count.EqualTo(1));
26+
Assert.That(command.Name, Is.EqualTo("convert-swagger"));
27+
Assert.That(command.Description, Does.Contain("Convert an existing Azure service swagger definition to a TypeSpec project"));
2928
});
3029
}
3130

@@ -34,12 +33,12 @@ public async Task ConvertSwagger_WithInvalidFileExtension_ShouldReturnError()
3433
{
3534
// Arrange
3635
var npxHelper = new Mock<INpxHelper>().Object;
37-
var logger = new Mock<ILogger<TypeSpecTool>>().Object;
36+
var logger = new Mock<ILogger<TypeSpecConvertTool>>().Object;
3837
var outputService = new Mock<IOutputHelper>().Object;
39-
var tool = new TypeSpecTool(npxHelper, logger, outputService);
38+
var tool = new TypeSpecConvertTool(npxHelper, logger, outputService);
4039

4140
// Act
42-
var result = await tool.ConvertSwagger("swagger.json", @"C:\temp", false, false, false, CancellationToken.None);
41+
var result = await tool.ConvertSwaggerAsync("swagger.json", @"C:\temp", false, false, false, CancellationToken.None);
4342

4443
// Assert
4544
Assert.That(result.IsSuccessful, Is.False);
@@ -51,16 +50,13 @@ public async Task ConvertSwagger_WithNonExistentFile_ShouldReturnError()
5150
{
5251
// Arrange
5352
var npxHelper = new Mock<INpxHelper>().Object;
54-
var logger = new Mock<ILogger<TypeSpecTool>>().Object;
53+
var logger = new Mock<ILogger<TypeSpecConvertTool>>().Object;
5554
var outputService = new Mock<IOutputHelper>().Object;
56-
var tool = new TypeSpecTool(npxHelper, logger, outputService);
55+
var tool = new TypeSpecConvertTool(npxHelper, logger, outputService);
5756

5857
// Act
59-
var result = await tool.ConvertSwagger(@"C:\nonexistent\readme.md", @"C:\temp", false, false, false, CancellationToken.None);
60-
61-
// Assert
58+
var result = await tool.ConvertSwaggerAsync(@"C:\nonexistent\readme.md", @"C:\temp", false, false, false, CancellationToken.None);
6259
Assert.That(result.IsSuccessful, Is.False);
63-
Assert.That(result.ResponseError, Does.Contain("does not exist"));
6460
}
6561
}
6662
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
using Microsoft.Extensions.Logging;
4+
using Azure.Sdk.Tools.Cli.Tools.TypeSpec;
5+
using Moq;
6+
using Azure.Sdk.Tools.Cli.Helpers;
7+
8+
namespace Azure.Sdk.Tools.Cli.Tests.Tools
9+
{
10+
public class TspInitToolTests
11+
{
12+
[Test]
13+
public void GetCommand_ShouldReturnCommand()
14+
{
15+
// Arrange
16+
var npxHelper = new Mock<INpxHelper>().Object;
17+
var logger = new Mock<ILogger<TypeSpecInitTool>>().Object;
18+
var outputService = new Mock<IOutputHelper>().Object;
19+
var tool = new TypeSpecInitTool(npxHelper, logger, outputService);
20+
21+
// Act
22+
var command = tool.GetCommand();
23+
24+
Assert.Multiple(() =>
25+
{
26+
Assert.That(command.Name, Is.EqualTo("init"));
27+
Assert.That(command.Description, Does.Contain("Initialize a new TypeSpec project"));
28+
});
29+
}
30+
31+
[Test]
32+
public async Task Init_WithInvalidTemplate_ShouldReturnError()
33+
{
34+
var npxHelper = new Mock<INpxHelper>().Object;
35+
var logger = new Mock<ILogger<TypeSpecInitTool>>().Object;
36+
var outputService = new Mock<IOutputHelper>().Object;
37+
var tool = new TypeSpecInitTool(npxHelper, logger, outputService);
38+
39+
var result = await tool.InitTypeSpecProjectAsync(outputDirectory: "never-used", template: "invalid-template", serviceNamespace: "MyService", isCli: false);
40+
41+
Assert.Multiple(() =>
42+
{
43+
Assert.That(result.IsSuccessful, Is.False);
44+
Assert.That(result.ResponseError, Does.Contain("Invalid --template"));
45+
});
46+
47+
}
48+
49+
[Test]
50+
public async Task Init_WithInvalidServiceNamespace_ShouldReturnError()
51+
{
52+
var npxHelper = new Mock<INpxHelper>().Object;
53+
var logger = new Mock<ILogger<TypeSpecInitTool>>().Object;
54+
var outputService = new Mock<IOutputHelper>().Object;
55+
var tool = new TypeSpecInitTool(npxHelper, logger, outputService);
56+
57+
var result = await tool.InitTypeSpecProjectAsync(outputDirectory: "never-used", template: "azure-core", serviceNamespace: "", isCli: false);
58+
59+
Assert.Multiple(() =>
60+
{
61+
Assert.That(result.IsSuccessful, Is.False);
62+
Assert.That(result.ResponseError, Does.Contain("Invalid --service-namespace"));
63+
});
64+
}
65+
66+
[Test]
67+
public async Task Init_WithNonExistentDirectory_ShouldReturnError()
68+
{
69+
var npxHelper = new Mock<INpxHelper>().Object;
70+
var logger = new Mock<ILogger<TypeSpecInitTool>>().Object;
71+
var outputService = new Mock<IOutputHelper>().Object;
72+
var tool = new TypeSpecInitTool(npxHelper, logger, outputService);
73+
74+
var result = await tool.InitTypeSpecProjectAsync(outputDirectory: Path.Combine(Path.GetTempPath(), $"test-nonexistent-{Guid.NewGuid()}"), template: "azure-core", serviceNamespace: "MyService", isCli: false);
75+
76+
Assert.Multiple(() =>
77+
{
78+
Assert.That(result.IsSuccessful, Is.False);
79+
Assert.That(result.ResponseError, Does.Contain("Invalid --output-directory"));
80+
});
81+
}
82+
}
83+
}

tools/azsdk-cli/Azure.Sdk.Tools.Cli/Commands/SharedCommandGroups.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,18 @@ public static class SharedCommandGroups
4141
Options: []
4242
);
4343

44-
#if DEBUG
44+
public static readonly CommandGroup TypeSpec = new(
45+
Verb: "tsp",
46+
Description: "Tools for setting up or working with TypeSpec projects",
47+
Options: []
48+
);
49+
50+
#if DEBUG
4551
public static readonly CommandGroup Example = new(
4652
Verb: "example",
4753
Description: "Example tool demonstrating framework features",
4854
Options: []
4955
);
50-
#endif
56+
#endif
5157
}
5258
}

tools/azsdk-cli/Azure.Sdk.Tools.Cli/Commands/SharedOptions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ public static class SharedOptions
3434
typeof(SpecWorkflowTool),
3535
typeof(SpecValidationTools),
3636
typeof(TestAnalysisTool),
37-
typeof(TypeSpecTool),
37+
typeof(TypeSpecConvertTool),
38+
typeof(TypeSpecInitTool),
3839
typeof(TypeSpecPublicRepoValidationTool),
3940

4041
#if DEBUG

tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/TypeSpec/TspTool.cs renamed to tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/TypeSpec/TspConvertTool.cs

Lines changed: 47 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,89 +8,89 @@
88
using Azure.Sdk.Tools.Cli.Models.Responses;
99
using ModelContextProtocol.Server;
1010
using Azure.Sdk.Tools.Cli.Helpers;
11+
using Azure.Sdk.Tools.Cli.Commands;
1112

1213
namespace Azure.Sdk.Tools.Cli.Tools.TypeSpec
1314
{
1415
/// <summary>
15-
/// This tool provides functionality for initializing TypeSpec projects and converting existing Azure service swagger definitions to TypeSpec projects.
16-
/// Use this tool to onboard to TypeSpec for new services or convert existing services.
16+
/// This tool provides functionality for converting existing Azure service swagger definitions to TypeSpec projects.
17+
/// Use this tool to convert existing services to TypeSpec.
1718
/// </summary>
18-
[McpServerToolType, Description("Tools for initializing TypeSpec projects and converting existing Azure service swagger definitions to TypeSpec projects.")]
19-
public class TypeSpecTool(INpxHelper npxHelper, ILogger<TypeSpecTool> logger, IOutputHelper output) : MCPTool
19+
[McpServerToolType, Description("Tools for converting existing Azure service swagger definitions to TypeSpec projects.")]
20+
public class TypeSpecConvertTool : MCPTool
2021
{
22+
private readonly INpxHelper npxHelper;
23+
private readonly ILogger<TypeSpecConvertTool> logger;
24+
private readonly IOutputHelper output;
25+
26+
public TypeSpecConvertTool(INpxHelper npxHelper, ILogger<TypeSpecConvertTool> logger, IOutputHelper output)
27+
{
28+
this.npxHelper = npxHelper;
29+
this.logger = logger;
30+
this.output = output;
31+
CommandHierarchy = [SharedCommandGroups.TypeSpec];
32+
}
2133

2234
// commands
2335
private const string ConvertSwaggerCommandName = "convert-swagger";
2436

37+
// command options
2538
private readonly Option<string> outputDirectoryArg = new("--output-directory", "The output directory for the generated TypeSpec project. This directory must already exist and be empty.") { IsRequired = true };
26-
2739
private readonly Option<string> swaggerReadmeArg = new("--swagger-readme", "The path or URL to an Azure swagger README file.") { IsRequired = true };
2840
private readonly Option<bool> isArmOption = new("--arm", "Whether the generated TypeSpec project is for an Azure Resource Management (ARM) API. This should be true if the swagger's path contains 'resource-manager'.");
2941
private readonly Option<bool> fullyCompatibleOption = new("--fully-compatible", "Whether to generate a TypeSpec project that is fully compatible with the swagger. It is recommended not to set this to true so that the converted TypeSpec project leverages TypeSpec built-in libraries with standard patterns and templates.");
3042

3143
public override Command GetCommand()
3244
{
33-
var tspCommand = new Command("tsp", "Tools for initializing TypeSpec projects and converting existing Azure service swagger definitions to TypeSpec projects");
3445

35-
var subCommands = new[]
36-
{
37-
new Command(ConvertSwaggerCommandName, "Convert an existing Azure service swagger definition to a TypeSpec project") {
38-
swaggerReadmeArg,
39-
outputDirectoryArg,
40-
isArmOption,
41-
fullyCompatibleOption
42-
}
46+
Command command = new(ConvertSwaggerCommandName, "Convert an existing Azure service swagger definition to a TypeSpec project") {
47+
swaggerReadmeArg,
48+
outputDirectoryArg,
49+
isArmOption,
50+
fullyCompatibleOption
4351
};
52+
command.SetHandler(async ctx => { await HandleCommand(ctx, ctx.GetCancellationToken()); });
4453

45-
foreach (var subCommand in subCommands)
46-
{
47-
subCommand.SetHandler(async ctx => { await HandleCommand(ctx, ctx.GetCancellationToken()); });
48-
tspCommand.AddCommand(subCommand);
49-
}
50-
51-
return tspCommand;
54+
return command;
5255
}
5356

5457
public override async Task HandleCommand(InvocationContext ctx, CancellationToken ct)
5558
{
56-
var command = ctx.ParseResult.CommandResult.Command.Name;
57-
switch (command)
58-
{
59-
case ConvertSwaggerCommandName:
60-
await HandleConvertCommand(ctx, ct);
61-
return;
62-
default:
63-
SetFailure();
64-
output.Output($"Unknown command: {command}");
65-
return;
66-
}
59+
await HandleConvertCommandAsync(ctx, ct);
6760
}
6861

69-
private async Task HandleConvertCommand(InvocationContext ctx, CancellationToken ct)
62+
private async Task HandleConvertCommandAsync(InvocationContext ctx, CancellationToken ct)
7063
{
7164
var swaggerReadme = ctx.ParseResult.GetValueForOption(swaggerReadmeArg);
7265
var outputDirectory = ctx.ParseResult.GetValueForOption(outputDirectoryArg);
7366
var isArm = ctx.ParseResult.GetValueForOption(isArmOption);
7467
var fullyCompatible = ctx.ParseResult.GetValueForOption(fullyCompatibleOption);
7568

76-
TspToolResponse result = await ConvertSwagger(swaggerReadme, outputDirectory, isArm, fullyCompatible, true, ct);
69+
TspToolResponse result = await ConvertSwaggerAsync(swaggerReadme, outputDirectory, isArm, fullyCompatible, true, ct);
7770
ctx.ExitCode = ExitCode;
7871
output.Output(result);
7972
}
8073

81-
[McpServerTool(Name = "azsdk_convert_swagger_to_typespec"), Description(@"Converts an existing Azure service swagger definition to a TypeSpec project.
82-
Pass in the `pathToSwaggerReadme` which is the path to the swagger README file.
83-
Pass in the `outputDirectory` where the TypeSpec project should be created. This must be an existing empty directory.
84-
Pass in `isAzureResourceManagement` to indicate whether the swagger is for an Azure Resource Management (ARM) API.
85-
This should be true if the swagger's path contains `resource-manager`.
86-
Pass in `fullyCompatible` to indicate whether the generated TypeSpec project should be fully compatible with the swagger.
87-
It is recommended not to set this to `true` so that the converted TypeSpec project
88-
leverages TypeSpec built-in libraries with standard patterns and templates.
89-
Returns path to the created project.")]
90-
public async Task<TspToolResponse> ConvertSwagger(
74+
[
75+
McpServerTool(Name = "azsdk_convert_swagger_to_typespec"),
76+
Description("Converts an existing Azure service swagger definition to a TypeSpec project. Returns path to the created project.")
77+
]
78+
public async Task<TspToolResponse> ConvertSwaggerAsync(
79+
[Description("Path to the swagger README file.")]
9180
string pathToSwaggerReadme,
81+
[Description("The output directory for the generated TypeSpec project. This must be an existing empty directory.")]
9282
string outputDirectory,
83+
[Description(@"
84+
Indicates whether the swagger is for an Azure Resource Management (ARM) API.
85+
Should be true if the swagger's path contains `resource-manager`.
86+
")
87+
]
9388
bool? isAzureResourceManagement,
89+
[Description(@"
90+
Indicates whether the generated TypeSpec project should be fully compatible with the swagger.
91+
It is recommended to set this to `false` so that the generated project leverages TypeSpec built-in libraries with standard patterns and templates.
92+
")
93+
]
9494
bool? fullyCompatible,
9595
bool isCli,
9696
CancellationToken ct
@@ -121,12 +121,12 @@ CancellationToken ct
121121
SetFailure();
122122
return new TspToolResponse
123123
{
124-
ResponseError = $"Failed: Invalid --output-dir, {validationResult}"
124+
ResponseError = $"Failed: Invalid --output-directory, {validationResult}"
125125
};
126126
}
127127

128128
var fullOutputDir = Path.GetFullPath(outputDirectory.Trim());
129-
return await RunTspClient(fullPathToSwaggerReadme, fullOutputDir, isAzureResourceManagement ?? false, fullyCompatible ?? false, isCli, ct);
129+
return await RunTspClientAsync(fullPathToSwaggerReadme, fullOutputDir, isAzureResourceManagement ?? false, fullyCompatible ?? false, isCli, ct);
130130
}
131131
catch (Exception ex)
132132
{
@@ -155,14 +155,13 @@ CancellationToken ct
155155
var fullPathToSwaggerReadme = Path.GetFullPath(pathToSwaggerReadme.Trim());
156156
if (!File.Exists(fullPathToSwaggerReadme))
157157
{
158-
159158
return $"Failed: pathToSwaggerReadme '{fullPathToSwaggerReadme}' does not exist.";
160159
}
161160

162161
return null; // Validation passed
163162
}
164163

165-
private async Task<TspToolResponse> RunTspClient(
164+
private async Task<TspToolResponse> RunTspClientAsync(
166165
string pathToSwaggerReadme,
167166
string outputDirectory,
168167
bool isAzureResourceManagement,

0 commit comments

Comments
 (0)