Skip to content

Commit eb83798

Browse files
committed
Add path and openApiEndpoint options
1 parent bd0a7b5 commit eb83798

File tree

9 files changed

+118
-17
lines changed

9 files changed

+118
-17
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## 0.0.2 (2025-11-17)
4+
5+
### Enhancements
6+
7+
- Add `path` option to be able to give an absolute or relative path to the web app
8+
- Add `openApiEndpoint` option to be able to set a custom endpoint
9+
310
## 0.0.1 (2025-11-15)
411

512
- Initial release

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
# RouteCheck
44

5+
[![NuGet Package](https://img.shields.io/nuget/vpre/RouteCheck)](https://nuget.org/packages/RouteCheck)
56
[![License](https://img.shields.io/github/license/eisnstein/RouteCheck)](https://github.com/eisnstein/RouteCheck/blob/main/LICENSE)
67

78
Check your API routes in your Terminal.
@@ -36,6 +37,22 @@ This should give you something like this:
3637

3738
![RouteCheck check example](https://github.com/eisnstein/RouteCheck/blob/main/src/Assets/routecheck-check.png)
3839

40+
You can also provide a path to the web app via the `path` option:
41+
42+
```sh
43+
routecheck --path /absolute/path/to/webapp
44+
45+
# or
46+
47+
routecheck -p relativ/path/to/webapp
48+
```
49+
50+
If you use a custom OpenApi endpoint, you can set the `openApiEndpoint` option:
51+
52+
```sh
53+
routecheck --openApiEndpoint /some/other/endpoint
54+
```
55+
3956
For help run:
4057

4158
```sh

src/Commands/CheckCommand.cs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using RouteCheck.Commands.Settings;
22
using RouteCheck.Services;
33

4+
using Spectre.Console;
45
using Spectre.Console.Cli;
56

67
namespace RouteCheck.Commands;
@@ -13,14 +14,28 @@ public CheckCommand()
1314

1415
public override async Task<int> ExecuteAsync(
1516
CommandContext _context,
16-
CheckSettings _settings,
17+
CheckSettings settings,
1718
CancellationToken _cancellationToken)
1819
{
19-
var cwd = Directory.GetCurrentDirectory();
20-
var (webApp, port) = await WebAppService.StartWebApp(cwd);
21-
var openApiDoc = await OpenApiService.GetOpenApiJsonAsync(port);
20+
string cwd = Directory.GetCurrentDirectory();
21+
string pathToProject = Path.Combine(cwd, settings.Path ?? "");
22+
if (!File.Exists(Path.Combine(pathToProject, "Program.cs")))
23+
{
24+
AnsiConsole.MarkupLine($"[red]Error:[/] The specified path '{pathToProject}' does not appear to be a valid .NET web application project directory. Missing 'Program.cs' file.");
25+
return -1;
26+
}
2227

23-
OutputService.DisplayRoutesFromSwagger(openApiDoc);
28+
AnsiConsole.MarkupLine($"Starting web application from path: [grey]{pathToProject}[/] and waiting to be ready...");
29+
30+
var (webApp, port) = await WebAppService.StartWebApp(pathToProject);
31+
32+
AnsiConsole.MarkupLine($"Web application is running on port [grey]{port}[/]. Retrieving OpenAPI document...");
33+
34+
var openApiDoc = await OpenApiService.GetOpenApiJsonAsync(port, settings.OpenApiEndpoint);
35+
36+
Console.WriteLine();
37+
38+
OutputService.DisplayRoutesFromOpenApi(openApiDoc);
2439
WebAppService.StopWebApp(webApp);
2540

2641
return 1;

src/Commands/Settings/CheckSettings.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ namespace RouteCheck.Commands.Settings;
66

77
public sealed class CheckSettings : CommandSettings
88
{
9-
[CommandOption("--csprojFile <Path>")]
10-
[Description(@"Path to *.csproj file. (default .\*.csproj)")]
11-
public string? PathToCsProjFile { get; set; }
9+
[CommandOption("-p|--path <Path>")]
10+
[Description(@"Path to web app for which to check routes. If not specified, the current directory will be used. Can be a relative or absolute path.")]
11+
public string? Path { get; set; }
12+
13+
[CommandOption("--openApiEndpoint <OpenApiEndpoint>")]
14+
[Description(@"OpenApi endpoint. If not specified, the default '/openapi/v1.json' will be used.")]
15+
[DefaultValue("/openapi/v1.json")]
16+
public required string OpenApiEndpoint { get; set; }
1217
}

src/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
config.AddCommand<CheckCommand>("check")
2525
.WithAlias("c")
2626
.WithDescription("Check your routes. (default command)")
27-
.WithExample(["check", "--csprojFile", ".\\examples\\csproj.xml"]);
27+
.WithExample(["check", "--path", "/path/to/webapp"]);
2828
});
2929

3030
return await app.RunAsync(args)

src/RouteCheck.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
1212
<ToolCommandName>routecheck</ToolCommandName>
1313
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
14-
<Version>0.0.1</Version>
14+
<Version>0.0.2</Version>
1515
</PropertyGroup>
1616

1717
<PropertyGroup Label="Package Information">
@@ -21,7 +21,7 @@
2121
<PackageIcon>icon.png</PackageIcon>
2222
<PackageId>RouteCheck</PackageId>
2323
<PackageLicenseExpression>MIT</PackageLicenseExpression>
24-
<PackageVersion>0.0.1</PackageVersion>
24+
<PackageVersion>0.0.2</PackageVersion>
2525
<RepositoryType>git</RepositoryType>
2626
<RepositoryUrl>https://github.com/eisnstein/RouteCheck</RepositoryUrl>
2727
<PackageReadmeFile>README.md</PackageReadmeFile>

src/Services/OpenApiService.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,27 @@
22

33
public static class OpenApiService
44
{
5-
public static async Task<OpenApiDocument> GetOpenApiJsonAsync(int port)
5+
public static async Task<OpenApiDocument> GetOpenApiJsonAsync(int port, string endpoint = "openapi/v1.json")
66
{
7-
var (openApiDoc, _) = await OpenApiDocument.LoadAsync($"http://localhost:{port}/openapi/v1.json");
7+
var url = await BuildUrlAsync(port, endpoint);
8+
9+
var (openApiDoc, _) = await OpenApiDocument.LoadAsync(url);
810
if (openApiDoc is null)
911
{
1012
throw new Exception("Failed to load OpenAPI document.");
1113
}
1214

1315
return openApiDoc;
1416
}
17+
18+
private static async Task<string> BuildUrlAsync(int port, string endpoint)
19+
{
20+
var url = new UriBuilder
21+
{
22+
Port = port,
23+
Path = endpoint
24+
}.Uri;
25+
26+
return url.ToString();
27+
}
1528
}

src/Services/OutputService.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@ namespace RouteCheck.Services;
66

77
public static class OutputService
88
{
9-
public static void DisplayRoutesFromSwagger(OpenApiDocument openApiDoc)
9+
public static void DisplayRoutesFromOpenApi(OpenApiDocument openApiDoc)
1010
{
1111
var table = new Table();
1212
table.Border = TableBorder.Ascii2;
1313
table.AddColumn("Method");
1414
table.AddColumn("Path");
1515

16-
AnsiConsole.MarkupLine("Routes for [grey]API[/]:");
1716
foreach (KeyValuePair<string, IOpenApiPathItem> path in openApiDoc.Paths)
1817
{
1918
if (path.Value.Operations is null)

src/Services/WebAppService.cs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44

55
public static class WebAppService
66
{
7-
public static async Task<(Process webApp, int port)> StartWebApp(string projectPath, int? port = null)
7+
public static async Task<(Process webApp, int port)> StartWebApp(string projectPath, int? port = null, TimeSpan? startupTimeout = null)
88
{
99
port ??= GetAvailablePort();
1010
Process process = StartProcess("dotnet", $"run --urls=http://localhost:{port}", projectPath);
11-
await Task.Delay(5000); // Allow app to start
11+
12+
await WaitForWebAppReadyAsync(process, port.Value, startupTimeout ?? TimeSpan.FromSeconds(20));
1213

1314
return (process, port.Value);
1415
}
@@ -50,4 +51,48 @@ private static Process StartProcess(string fileName, string arguments, string wo
5051

5152
return process;
5253
}
54+
55+
private static async Task WaitForWebAppReadyAsync(Process process, int port, TimeSpan timeout)
56+
{
57+
var stopwatch = Stopwatch.StartNew();
58+
59+
while (stopwatch.Elapsed < timeout)
60+
{
61+
if (process.HasExited)
62+
{
63+
throw new InvalidOperationException("The web application process exited before it started accepting requests.");
64+
}
65+
66+
if (await IsPortOpenAsync(port))
67+
{
68+
return;
69+
}
70+
71+
await Task.Delay(250);
72+
}
73+
74+
throw new TimeoutException($"Timed out waiting for the web application to start listening on port {port}.");
75+
}
76+
77+
private static async Task<bool> IsPortOpenAsync(int port)
78+
{
79+
try
80+
{
81+
using var client = new TcpClient();
82+
var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
83+
var completed = await Task.WhenAny(connectTask, Task.Delay(500));
84+
85+
if (completed == connectTask)
86+
{
87+
await connectTask;
88+
return true;
89+
}
90+
}
91+
catch (SocketException)
92+
{
93+
// Port is not accepting connections yet
94+
}
95+
96+
return false;
97+
}
5398
}

0 commit comments

Comments
 (0)