Skip to content

Commit 7dca78f

Browse files
Add validation command
1 parent 6ed9b10 commit 7dca78f

File tree

12 files changed

+2607
-71765
lines changed

12 files changed

+2607
-71765
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System.Text;
2+
using System.Text.Json;
3+
using System.Text.Json.Nodes;
4+
using HotChocolate.Adapters.OpenApi;
5+
using HotChocolate.Adapters.OpenApi.Packaging;
6+
using HotChocolate.Language;
7+
8+
namespace ChilliCream.Nitro.CommandLine.Cloud.Commands.OpenApi;
9+
10+
internal static class OpenApiCollectionHelpers
11+
{
12+
public static async Task<MemoryStream> BuildOpenApiCollectionArchive(
13+
IEnumerable<string> files,
14+
CancellationToken cancellationToken)
15+
{
16+
var archiveStream = new MemoryStream();
17+
var collectionArchive = OpenApiCollectionArchive.Create(archiveStream, leaveOpen: true);
18+
19+
await collectionArchive.SetArchiveMetadataAsync(
20+
new ArchiveMetadata(),
21+
cancellationToken);
22+
23+
var parser = new OpenApiDocumentParser();
24+
25+
foreach (var file in files)
26+
{
27+
var fileContent = await File.ReadAllBytesAsync(file, cancellationToken);
28+
var document = Utf8GraphQLParser.Parse(fileContent);
29+
// TODO: The id doesn't mean anything, we should probably get rid of it...
30+
var openApiDocumentDefinition = new OpenApiDocumentDefinition(file, document);
31+
32+
var parseResult = parser.Parse(openApiDocumentDefinition);
33+
34+
if (!parseResult.IsValid)
35+
{
36+
// TODO: Handle properly
37+
continue;
38+
}
39+
40+
if (parseResult.Document is OpenApiOperationDocument operationDocument)
41+
{
42+
var operationBytes = Encoding.UTF8.GetBytes(operationDocument.OperationDefinition.ToString());
43+
var settings = CreateJsonSettingsForOperationDocument(operationDocument);
44+
45+
await collectionArchive.AddOpenApiEndpointAsync(
46+
operationDocument.Name,
47+
operationBytes,
48+
settings,
49+
cancellationToken);
50+
}
51+
else if (parseResult.Document is OpenApiFragmentDocument fragmentDocument)
52+
{
53+
var fragmentBytes = Encoding.UTF8.GetBytes(fragmentDocument.FragmentDefinition.ToString());
54+
55+
await collectionArchive.AddOpenApiModelAsync(
56+
fragmentDocument.Name,
57+
fragmentBytes,
58+
cancellationToken);
59+
}
60+
else
61+
{
62+
throw new NotSupportedException();
63+
}
64+
}
65+
66+
await collectionArchive.CommitAsync(cancellationToken);
67+
collectionArchive.Dispose();
68+
69+
return archiveStream;
70+
}
71+
72+
private static JsonDocument CreateJsonSettingsForOperationDocument(OpenApiOperationDocument document)
73+
{
74+
var obj = new JsonObject();
75+
obj.Add("httpMethod", document.HttpMethod);
76+
obj.Add("route", document.Route.ToOpenApiPath());
77+
78+
// TODO: Add other settings
79+
80+
using var stream = new MemoryStream();
81+
using (var writer = new Utf8JsonWriter(stream))
82+
{
83+
obj.WriteTo(writer);
84+
}
85+
86+
stream.Position = 0;
87+
88+
return JsonDocument.Parse(stream);
89+
}
90+
}

src/Nitro/CommandLine/src/CommandLine.Cloud/Commands/OpenApi/OpenApiCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ public OpenApiCommand() : base("openapi")
1313
AddCommand(new ListOpenApiCollectionCommand());
1414
AddCommand(new UploadOpenApiCollectionCommand());
1515
AddCommand(new PublishOpenApiCollectionCommand());
16+
AddCommand(new ValidateOpenApiCollectionCommand());
1617
}
1718
}
Lines changed: 5 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
1-
using System.Collections.Immutable;
2-
using System.Text;
3-
using System.Text.Json;
4-
using System.Text.Json.Nodes;
51
using ChilliCream.Nitro.CommandLine.Cloud.Client;
2+
using ChilliCream.Nitro.CommandLine.Cloud.Commands.OpenApi;
63
using ChilliCream.Nitro.CommandLine.Cloud.Option;
74
using ChilliCream.Nitro.CommandLine.Cloud.Option.Binders;
8-
using HotChocolate.Adapters.OpenApi;
9-
using HotChocolate.Adapters.OpenApi.Packaging;
10-
using HotChocolate.Language;
11-
using Microsoft.Extensions.FileSystemGlobbing;
12-
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
135
using StrawberryShake;
146
using Command = System.CommandLine.Command;
157

@@ -23,21 +15,14 @@ public UploadOpenApiCollectionCommand() : base("upload")
2315

2416
AddOption(Opt<TagOption>.Instance);
2517
AddOption(Opt<OpenApiCollectionIdOption>.Instance);
26-
27-
var patternsOption = new Option<List<string>>("--patterns")
28-
{
29-
Description = "TODO"
30-
};
31-
patternsOption.AddAlias("-p");
32-
33-
AddOption(patternsOption);
18+
AddOption(Opt<OpenApiCollectionFilePatternOption>.Instance);
3419

3520
this.SetHandler(
3621
ExecuteAsync,
3722
Bind.FromServiceProvider<IAnsiConsole>(),
3823
Bind.FromServiceProvider<IApiClient>(),
3924
Opt<TagOption>.Instance,
40-
patternsOption,
25+
Opt<OpenApiCollectionFilePatternOption>.Instance,
4126
Opt<OpenApiCollectionIdOption>.Instance,
4227
Bind.FromServiceProvider<CancellationToken>());
4328
}
@@ -78,57 +63,8 @@ async Task UploadOpenApiCollection(StatusContext? ctx)
7863
return;
7964
}
8065

81-
var archiveStream = new MemoryStream();
82-
var collectionArchive = OpenApiCollectionArchive.Create(archiveStream, leaveOpen: true);
83-
84-
await collectionArchive.SetArchiveMetadataAsync(
85-
new ArchiveMetadata(),
86-
cancellationToken);
87-
88-
var parser = new OpenApiDocumentParser();
89-
90-
foreach (var file in files)
91-
{
92-
var fileContent = await File.ReadAllBytesAsync(file, cancellationToken);
93-
var document = Utf8GraphQLParser.Parse(fileContent);
94-
// TODO: The id doesn't mean anything, we should probably get rid of it...
95-
var openApiDocumentDefinition = new OpenApiDocumentDefinition(file, document);
96-
97-
var parseResult = parser.Parse(openApiDocumentDefinition);
98-
99-
if (!parseResult.IsValid)
100-
{
101-
// TODO: Handle properly
102-
continue;
103-
}
104-
105-
if (parseResult.Document is OpenApiOperationDocument operationDocument)
106-
{
107-
var operationBytes = Encoding.UTF8.GetBytes(operationDocument.OperationDefinition.ToString());
108-
var settings = CreateJsonSettingsForOperationDocument(operationDocument);
109-
110-
await collectionArchive.AddOpenApiEndpointAsync(
111-
operationDocument.Name,
112-
operationBytes,
113-
settings,
114-
cancellationToken);
115-
}
116-
else if (parseResult.Document is OpenApiFragmentDocument fragmentDocument)
117-
{
118-
var fragmentBytes = Encoding.UTF8.GetBytes(fragmentDocument.FragmentDefinition.ToString());
119-
120-
await collectionArchive.AddOpenApiModelAsync(
121-
fragmentDocument.Name,
122-
fragmentBytes,
123-
cancellationToken);
124-
}
125-
else
126-
{
127-
throw new NotSupportedException();
128-
}
129-
}
130-
131-
await collectionArchive.CommitAsync(cancellationToken);
66+
var archiveStream =
67+
await OpenApiCollectionHelpers.BuildOpenApiCollectionArchive(files, cancellationToken);
13268

13369
var input = new UploadOpenApiCollectionInput
13470
{
@@ -152,23 +88,4 @@ await collectionArchive.AddOpenApiModelAsync(
15288
console.Success("Successfully uploaded new OpenAPI collection version!");
15389
}
15490
}
155-
156-
private static JsonDocument CreateJsonSettingsForOperationDocument(OpenApiOperationDocument document)
157-
{
158-
var obj = new JsonObject();
159-
obj.Add("httpMethod", document.HttpMethod);
160-
obj.Add("route", document.Route.ToOpenApiPath());
161-
162-
// TODO: Add other settings
163-
164-
using var stream = new MemoryStream();
165-
using (var writer = new Utf8JsonWriter(stream))
166-
{
167-
obj.WriteTo(writer);
168-
}
169-
170-
stream.Position = 0;
171-
172-
return JsonDocument.Parse(stream);
173-
}
17491
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
using System.Reactive;
2+
using System.Reactive.Linq;
3+
using System.Reactive.Subjects;
4+
using ChilliCream.Nitro.CommandLine.Cloud.Client;
5+
using ChilliCream.Nitro.CommandLine.Cloud.Commands.OpenApi;
6+
using ChilliCream.Nitro.CommandLine.Cloud.Option;
7+
using ChilliCream.Nitro.CommandLine.Cloud.Option.Binders;
8+
using StrawberryShake;
9+
using static ChilliCream.Nitro.CommandLine.Cloud.ThrowHelper;
10+
11+
namespace ChilliCream.Nitro.CommandLine.Cloud;
12+
13+
internal sealed class ValidateOpenApiCollectionCommand : Command
14+
{
15+
public ValidateOpenApiCollectionCommand() : base("validate")
16+
{
17+
Description = "Validate an OpenAPI collection version";
18+
19+
AddOption(Opt<StageNameOption>.Instance);
20+
AddOption(Opt<OpenApiCollectionIdOption>.Instance);
21+
AddOption(Opt<OpenApiCollectionFilePatternOption>.Instance);
22+
23+
this.SetHandler(
24+
ExecuteAsync,
25+
Bind.FromServiceProvider<IAnsiConsole>(),
26+
Bind.FromServiceProvider<IApiClient>(),
27+
Opt<StageNameOption>.Instance,
28+
Opt<OpenApiCollectionIdOption>.Instance,
29+
Opt<OpenApiCollectionFilePatternOption>.Instance,
30+
Bind.FromServiceProvider<CancellationToken>());
31+
}
32+
33+
private static async Task<int> ExecuteAsync(
34+
IAnsiConsole console,
35+
IApiClient client,
36+
string stage,
37+
string openApiCollectionId,
38+
List<string> patterns,
39+
CancellationToken ct)
40+
{
41+
console.Title($"Validate against {stage.EscapeMarkup()}");
42+
43+
var isValid = false;
44+
45+
if (console.IsHumanReadable())
46+
{
47+
await console
48+
.Status()
49+
.Spinner(Spinner.Known.BouncingBar)
50+
.SpinnerStyle(Style.Parse("green bold"))
51+
.StartAsync("Validating...", ValidateOpenApiCollection);
52+
}
53+
else
54+
{
55+
await ValidateOpenApiCollection(null);
56+
}
57+
58+
return isValid ? ExitCodes.Success : ExitCodes.Error;
59+
60+
async Task ValidateOpenApiCollection(StatusContext? ctx)
61+
{
62+
// TODO: Print patterns for confirmation
63+
64+
var files = GlobMatcher.Match(patterns).ToArray();
65+
66+
if (files.Length < 1)
67+
{
68+
// TODO: Improve this error
69+
console.ErrorLine("Did not find any matches...");
70+
return;
71+
}
72+
73+
var archiveStream =
74+
await OpenApiCollectionHelpers.BuildOpenApiCollectionArchive(files, ct);
75+
76+
var input = new ValidateOpenApiCollectionInput()
77+
{
78+
OpenApiCollectionId = openApiCollectionId,
79+
Stage = stage,
80+
Collection = new Upload(archiveStream, "collection.zip")
81+
};
82+
83+
var requestId = await ValidateAsync(console, client, input, ct);
84+
85+
console.Log($"Validation request created [grey](ID: {requestId.EscapeMarkup()})[/]");
86+
87+
using var stopSignal = new Subject<Unit>();
88+
89+
var subscription = client.ValidateOpenApiCollectionCommandSubscription
90+
.Watch(requestId, ExecutionStrategy.NetworkOnly)
91+
.TakeUntil(stopSignal);
92+
93+
await foreach (var x in subscription.ToAsyncEnumerable().WithCancellation(ct))
94+
{
95+
if (x.Errors is { Count: > 0 } errors)
96+
{
97+
console.PrintErrorsAndExit(errors);
98+
throw Exit("No request id returned");
99+
}
100+
101+
switch (x.Data?.OnOpenApiCollectionVersionValidationUpdate)
102+
{
103+
case IOpenApiCollectionVersionValidationFailed { Errors: var schemaErrors }:
104+
console.ErrorLine("The OpenAPI collection is invalid:");
105+
console.PrintErrorsAndExit(schemaErrors);
106+
stopSignal.OnNext(Unit.Default);
107+
break;
108+
109+
case IOpenApiCollectionVersionValidationSuccess:
110+
isValid = true;
111+
stopSignal.OnNext(Unit.Default);
112+
console.Success("OpenAPI collection validation succeeded");
113+
break;
114+
115+
case IOperationInProgress:
116+
case IValidationInProgress:
117+
ctx?.Status("The validation is in progress.");
118+
break;
119+
120+
default:
121+
ctx?.Status(
122+
"This is an unknown response, upgrade Nitro CLI to the latest version.");
123+
break;
124+
}
125+
}
126+
}
127+
}
128+
129+
private static async Task<string> ValidateAsync(
130+
IAnsiConsole console,
131+
IApiClient client,
132+
ValidateOpenApiCollectionInput input,
133+
CancellationToken ct)
134+
{
135+
var result =
136+
await client.ValidateOpenApiCollectionCommandMutation.ExecuteAsync(input, ct);
137+
138+
console.EnsureNoErrors(result);
139+
var data = console.EnsureData(result);
140+
console.PrintErrorsAndExit(data.ValidateOpenApiCollection.Errors);
141+
142+
if (data.ValidateOpenApiCollection.Id is null)
143+
{
144+
throw new ExitException("Could not create validation request!");
145+
}
146+
147+
return data.ValidateOpenApiCollection.Id;
148+
}
149+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
mutation ValidateOpenApiCollectionCommandMutation($input: ValidateOpenApiCollectionInput!) {
2+
validateOpenApiCollection(input: $input) {
3+
id
4+
errors {
5+
...UnauthorizedOperation
6+
...StageNotFoundError
7+
...OpenApiCollectionNotFoundError
8+
...Error
9+
}
10+
}
11+
}
12+
13+
subscription ValidateOpenApiCollectionCommandSubscription($requestId: ID!) {
14+
onOpenApiCollectionVersionValidationUpdate(requestId: $requestId) {
15+
__typename
16+
...OpenApiCollectionVersionValidationFailed
17+
...OpenApiCollectionVersionValidationSuccess
18+
...OperationInProgress
19+
...ValidationInProgress
20+
}
21+
}

0 commit comments

Comments
 (0)