2using System.Collections;
3using System.Collections.Generic;
7using System.Reflection;
8using System.Text.RegularExpressions;
10using Microsoft.Extensions.DependencyInjection;
11using Microsoft.Net.Http.Headers;
12using Microsoft.OpenApi.Any;
13using Microsoft.OpenApi.Models;
15using Swashbuckle.AspNetCore.SwaggerGen;
62 public static void Configure(SwaggerGenOptions swaggerGenOptions,
string assemblyDocumentationPath,
string apiDocumentationPath)
64 swaggerGenOptions.SwaggerDoc(
70 License =
new OpenApiLicense
73 Url =
new Uri(
"https://github.com/tgstation/tgstation-server/blob/dev/LICENSE"),
75 Contact =
new OpenApiContact
77 Name =
"/tg/station 13",
78 Url =
new Uri(
"https://github.com/tgstation"),
80 Description =
"A production scale tool for DreamMaker server management",
85 swaggerGenOptions.IncludeXmlComments(assemblyDocumentationPath);
86 swaggerGenOptions.IncludeXmlComments(apiDocumentationPath);
89 swaggerGenOptions.UseAllOfToExtendReferenceSchemas();
100 In = ParameterLocation.Header,
101 Type = SecuritySchemeType.Http,
102 Name = HeaderNames.Authorization,
103 Description =
"Username & Password authentication to obtain an JWT token when doing a (POST /api).",
109 In = ParameterLocation.Header,
110 Type = SecuritySchemeType.Http,
111 Name = HeaderNames.Authorization,
112 Description =
"OAuth2 based authentication.",
118 In = ParameterLocation.Header,
119 Type = SecuritySchemeType.Http,
120 Name = HeaderNames.Authorization,
121 Description =
"JWT Bearer token generated by the server (POST /api). You need this for most endpoints to work.",
123 BearerFormat =
"JWT",
133 var errorMessageContent =
new Dictionary<string, OpenApiMediaType>
136 MediaTypeNames.Application.Json,
139 Schema =
new OpenApiSchema
141 Reference =
new OpenApiReference
144 Type = ReferenceType.Schema,
151 void AddDefaultResponse(HttpStatusCode code, OpenApiResponse concrete)
153 string responseKey = $
"{(int)code}";
155 document.Components.Responses.Add(responseKey, concrete);
157 var referenceResponse =
new OpenApiResponse
159 Reference =
new OpenApiReference
161 Type = ReferenceType.Response,
166 foreach (var operation
in document.Paths.SelectMany(path => path.Value.Operations))
167 operation.Value.Responses.TryAdd(responseKey, referenceResponse);
170 AddDefaultResponse(HttpStatusCode.BadRequest,
new OpenApiResponse
172 Description =
"A badly formatted request was made. See error message for details.",
173 Content = errorMessageContent,
176 AddDefaultResponse(HttpStatusCode.Unauthorized,
new OpenApiResponse
178 Description =
"Invalid Authentication header.",
181 AddDefaultResponse(HttpStatusCode.Forbidden,
new OpenApiResponse
183 Description =
"User lacks sufficient permissions for the operation.",
186 AddDefaultResponse(HttpStatusCode.Conflict,
new OpenApiResponse
188 Description =
"A data integrity check failed while performing the operation. See error message for details.",
189 Content = errorMessageContent,
192 AddDefaultResponse(HttpStatusCode.NotAcceptable,
new OpenApiResponse
194 Description = $
"Invalid Accept header, TGS requires `{HeaderNames.Accept}: {MediaTypeNames.Application.Json}`.",
195 Content = errorMessageContent,
198 AddDefaultResponse(HttpStatusCode.InternalServerError,
new OpenApiResponse
200 Description = ErrorCode.InternalServerError.Describe(),
201 Content = errorMessageContent,
204 AddDefaultResponse(HttpStatusCode.ServiceUnavailable,
new OpenApiResponse
206 Description =
"The server may be starting up or shutting down.",
209 AddDefaultResponse(HttpStatusCode.NotImplemented,
new OpenApiResponse
211 Description = ErrorCode.RequiresPosixSystemIdentity.Describe(),
212 Content = errorMessageContent,
224 rootSchema.Nullable =
false;
226 var rootRequestSchema = rootSchemaId.EndsWith(
"Request", StringComparison.Ordinal);
227 var rootResponseSchema = rootSchemaId.EndsWith(
"Response", StringComparison.Ordinal);
228 var isPutRequest = rootSchemaId.EndsWith(
"CreateRequest", StringComparison.Ordinal);
230 Tuple<PropertyInfo, string, OpenApiSchema, IDictionary<string, OpenApiSchema>> GetTypeFromKvp(Type currentType, KeyValuePair<string, OpenApiSchema> kvp, IDictionary<string, OpenApiSchema> schemaDictionary)
232 var propertyInfo = currentType
234 .Single(x => x.Name.Equals(kvp.Key, StringComparison.OrdinalIgnoreCase));
243 var subSchemaStack =
new Stack<Tuple<PropertyInfo, string, OpenApiSchema, IDictionary<string, OpenApiSchema>>>(
247 x => GetTypeFromKvp(context.Type, x, rootSchema.Properties))
248 .Where(x => x.Item3.Reference ==
null));
250 while (subSchemaStack.Count > 0)
252 var tuple = subSchemaStack.Pop();
253 var subSchema = tuple.Item3;
255 var subSchemaPropertyInfo = tuple.Item1;
257 if (subSchema.Properties !=
null
258 && !subSchemaPropertyInfo
261 .Any(x => x == typeof(IEnumerable)))
262 foreach (var kvp in subSchema.Properties.Where(x => x.Value.Reference ==
null))
263 subSchemaStack.Push(GetTypeFromKvp(subSchemaPropertyInfo.PropertyType, kvp, subSchema.Properties));
265 var attributes = subSchemaPropertyInfo
266 .GetCustomAttributes();
267 var responsePresence = attributes
272 var requestOptions = attributes
274 .OrderBy(x => x.PutOnly)
277 if (requestOptions.Count == 0 && requestOptions.All(x => x.Presence ==
FieldPresence.Ignored && !x.PutOnly))
278 subSchema.ReadOnly =
true;
280 var subSchemaId = tuple.Item2;
281 var subSchemaOwningDictionary = tuple.Item4;
282 if (rootResponseSchema)
284 subSchema.Nullable = responsePresence ==
FieldPresence.Optional;
286 subSchemaOwningDictionary.Remove(subSchemaId);
288 else if (rootRequestSchema)
290 subSchema.Nullable =
true;
291 var lastOptionWasIgnored =
false;
292 foreach (var requestOption
in requestOptions)
294 var validForThisRequest = !requestOption.PutOnly || isPutRequest;
295 if (!validForThisRequest)
298 lastOptionWasIgnored =
false;
299 switch (requestOption.Presence)
302 lastOptionWasIgnored =
true;
305 subSchema.Nullable =
true;
308 subSchema.Nullable =
false;
311 throw new InvalidOperationException($
"Invalid FieldPresence: {requestOption.Presence}!");
315 if (lastOptionWasIgnored)
316 subSchemaOwningDictionary.Remove(subSchemaId);
319 && requestOptions.All(x => x.Presence ==
FieldPresence.Required && !x.PutOnly))
320 subSchema.Nullable = subSchemaId.Equals(
322 StringComparison.OrdinalIgnoreCase)
338 return "ShallowUserResponse";
341 return $
"Paginated{type.GenericTypeArguments.First().Name}";
347 public void Apply(OpenApiOperation operation, OperationFilterContext context)
349 ArgumentNullException.ThrowIfNull(operation);
350 ArgumentNullException.ThrowIfNull(context);
352 operation.OperationId = $
"{context.MethodInfo.DeclaringType!.Name}.{context.MethodInfo.Name}";
354 var authAttributes = context
357 .GetCustomAttributes(
true)
361 .GetCustomAttributes(
true))
364 if (authAttributes.Any())
366 var tokenScheme =
new OpenApiSecurityScheme
368 Reference =
new OpenApiReference
370 Type = ReferenceType.SecurityScheme,
375 operation.Security =
new List<OpenApiSecurityRequirement>()
379 { tokenScheme,
new List<string>() },
384 operation.Parameters.Insert(0,
new OpenApiParameter
386 Reference =
new OpenApiReference
388 Type = ReferenceType.Parameter,
392 else if (typeof(
TransferController).IsAssignableFrom(context.MethodInfo.DeclaringType))
394 operation.RequestBody =
new OpenApiRequestBody
396 Content =
new Dictionary<string, OpenApiMediaType>
399 MediaTypeNames.Application.Octet,
402 Schema =
new OpenApiSchema
413 var twoHundredResponseContents = operation.Responses[
"200"].Content;
414 var fileContent = twoHundredResponseContents[MediaTypeNames.Application.Json];
415 twoHundredResponseContents.Remove(MediaTypeNames.Application.Json);
416 twoHundredResponseContents.Add(MediaTypeNames.Application.Octet, fileContent);
421 var passwordScheme =
new OpenApiSecurityScheme
423 Reference =
new OpenApiReference
425 Type = ReferenceType.SecurityScheme,
430 var oAuthScheme =
new OpenApiSecurityScheme
432 Reference =
new OpenApiReference
434 Type = ReferenceType.SecurityScheme,
439 operation.Parameters.Add(
new OpenApiParameter
441 In = ParameterLocation.Header,
443 Description =
"The external OAuth service provider.",
444 Style = ParameterStyle.Simple,
445 Example =
new OpenApiString(
"Discord"),
446 Schema =
new OpenApiSchema
452 operation.Security =
new List<OpenApiSecurityRequirement>
470 public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
472 ArgumentNullException.ThrowIfNull(swaggerDoc);
473 ArgumentNullException.ThrowIfNull(context);
475 swaggerDoc.ExternalDocs =
new OpenApiExternalDocs
477 Description =
"API Usage Documentation",
478 Url =
new Uri(
"https://tgstation.github.io/tgstation-server/api.html"),
483 In = ParameterLocation.Header,
484 Name = ApiHeaders.InstanceIdHeader,
485 Description =
"The instance ID being accessed",
487 Style = ParameterStyle.Simple,
488 Schema = new OpenApiSchema
494 var productHeaderSchema =
new OpenApiSchema
501 In = ParameterLocation.Header,
502 Name = ApiHeaders.ApiVersionHeader,
503 Description =
"The API version being used in the form \"Tgstation.Server.Api/[API version]\"",
505 Style = ParameterStyle.Simple,
506 Example = new OpenApiString($
"Tgstation.Server.Api/{ApiHeaders.Version}"),
507 Schema = productHeaderSchema,
510 swaggerDoc.Components.Parameters.Add(HeaderNames.UserAgent,
new OpenApiParameter
512 In = ParameterLocation.Header,
513 Name = HeaderNames.UserAgent,
514 Description =
"The user agent of the calling client.",
516 Style = ParameterStyle.Simple,
517 Example = new OpenApiString(
"Your-user-agent/1.0.0.0"),
518 Schema = productHeaderSchema,
521 var allSchemas = context
524 foreach (var path
in swaggerDoc.Paths)
525 foreach (var operation
in path.Value.Operations.Select(x => x.Value))
527 operation.Parameters.Insert(0,
new OpenApiParameter
529 Reference =
new OpenApiReference
531 Type = ReferenceType.Parameter,
536 operation.Parameters.Insert(1,
new OpenApiParameter
538 Reference =
new OpenApiReference
540 Type = ReferenceType.Parameter,
541 Id = HeaderNames.UserAgent,
546 if (operation.Description?.Length > 0)
547 operation.Description = Regex.Replace(operation.Description,
@"Tgstation\.Server\.(\w+\.)+(?=\w+)",
string.Empty);
549 if (operation.Summary?.Length > 0)
550 operation.Summary = Regex.Replace(operation.Summary,
@"Tgstation\.Server\.(\w+\.)+(?=\w+)",
string.Empty);
552 if (operation.RequestBody?.Description !=
null)
553 operation.RequestBody.Description = Regex.Replace(operation.RequestBody.Description,
@"Tgstation\.Server\.(\w+\.)+(?=\w+)",
string.Empty);
555 foreach (var opPar
in operation.Parameters)
557 if (opPar.Description !=
null)
558 opPar.Description = Regex.Replace(opPar.Description,
@"Tgstation\.Server\.(\w+\.)+(?=\w+)",
string.Empty);
561 foreach (var (_, opRes) in operation.Responses)
563 if (opRes.Description !=
null)
564 opRes.Description = Regex.Replace(opRes.Description,
@"Tgstation\.Server\.(\w+\.)+(?=\w+)",
string.Empty);
572 public void Apply(OpenApiSchema schema, SchemaFilterContext context)
574 ArgumentNullException.ThrowIfNull(schema);
575 ArgumentNullException.ThrowIfNull(context);
578 schema.Required.Clear();
580 if (context.MemberInfo ==
null)
581 ApplyAttributesForRootSchema(schema, context);
583 if (!schema.Enum?.Any() ??
false)
587 Type firstGenericArgumentOrType = context.Type.IsConstructedGenericType
588 ? context.Type.GenericTypeArguments.First()
595 public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context)
597 ArgumentNullException.ThrowIfNull(requestBody);
598 ArgumentNullException.ThrowIfNull(context);
600 requestBody.Required =
true;
Indicates the FieldPresence for fields in models.
Represents an error message returned by the server.
Represents a paginated set of models.
Indicates the response FieldPresence of API fields. Changes it from FieldPresence....
FieldPresence Presence
The FieldPresence.
Parameters for creating a TestMerge.
virtual ? string TargetCommitSha
The sha of the test merge revision to merge. If not specified, the latest commit from the source will...
Base class for user names.
Root ApiController for the Application.
ValueTask< IActionResult > CreateToken(CancellationToken cancellationToken)
Attempt to authenticate a User using ApiController.ApiHeaders.
ComponentInterfacingController for operations that require an instance.
ApiController for file streaming.
ValueTask< IActionResult > Download([Required, FromQuery] string ticket, CancellationToken cancellationToken)
Downloads a file with a given ticket .
async ValueTask< IActionResult > Upload([Required, FromQuery] string ticket, CancellationToken cancellationToken)
Uploads a file with a given ticket .
Helper for using the AuthorizeAttribute with the Api.Rights system.
Implements the "x-enum-varnames" OpenAPI 3.0 extension.
static void Apply(OpenApiSchema openApiSchema, Type enumType)
Applies the extension to a give openApiSchema .
Implements various filters for Swashbuckle.
static string GenerateSchemaId(Type type)
Generates the OpenAPI schema ID for a given type .
void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context)
const string OAuthSecuritySchemeId
The OpenApiSecurityScheme name for OAuth 2.0 authentication.
const string DocumentationSiteRouteExtension
The path to the hosted documentation site.
void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
void Apply(OpenApiSchema schema, SchemaFilterContext context)
static void Configure(SwaggerGenOptions swaggerGenOptions, string assemblyDocumentationPath, string apiDocumentationPath)
Configure the swagger settings.
const string PasswordSecuritySchemeId
The OpenApiSecurityScheme name for password authentication.
const string TokenSecuritySchemeId
The OpenApiSecurityScheme name for token authentication.
const string DocumentName
The name of the swagger document.
static void AddDefaultResponses(OpenApiDocument document)
Add the default error responses to a given document .
void Apply(OpenApiOperation operation, OperationFilterContext context)
static void ApplyAttributesForRootSchema(OpenApiSchema rootSchema, SchemaFilterContext context)
Applies the OpenApiSchema.Nullable, OpenApiSchema.ReadOnly, and OpenApiSchema.WriteOnly to OpenApiSch...
FieldPresence
Indicates whether a request field is Required or Ignored.