jellyfin/Jellyfin.Server/Filters/AdditionalModelFilter.cs
Cody Robibero 9a0dfc00f1
Add all websocket messages to generated openapi spec (#9682)
* Add all websocket messages to generated openapi spec

* Use oneOf

* JsonIgnore ServerId

* Oops

* Add discriminators

* Add WebSocketMessage container for Inbound and Outbound messages
2023-06-10 07:28:21 -06:00

201 lines
9.4 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using Jellyfin.Extensions;
using Jellyfin.Server.Migrations;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Net.WebSocketMessages;
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
using MediaBrowser.Model.ApiClient;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters
{
/// <summary>
/// Add models not directly used by the API, but used for discovery and websockets.
/// </summary>
public class AdditionalModelFilter : IDocumentFilter
{
// Array of options that should not be visible in the api spec.
private static readonly Type[] _ignoredConfigurations = { typeof(MigrationOptions) };
private readonly IServerConfigurationManager _serverConfigurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="AdditionalModelFilter"/> class.
/// </summary>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
public AdditionalModelFilter(IServerConfigurationManager serverConfigurationManager)
{
_serverConfigurationManager = serverConfigurationManager;
}
/// <inheritdoc />
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
context.SchemaGenerator.GenerateSchema(typeof(IPlugin), context.SchemaRepository);
var webSocketTypes = typeof(WebSocketMessage).Assembly.GetTypes()
.Where(t => t.IsSubclassOf(typeof(WebSocketMessage))
&& !t.IsGenericType
&& t != typeof(WebSocketMessageInfo))
.ToList();
var inboundWebSocketSchemas = new List<OpenApiSchema>();
var inboundWebSocketDiscriminators = new Dictionary<string, string>();
foreach (var type in webSocketTypes.Where(t => typeof(IInboundWebSocketMessage).IsAssignableFrom(t)))
{
var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute<DefaultValueAttribute>()?.Value;
if (messageType is null)
{
continue;
}
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
inboundWebSocketSchemas.Add(schema);
inboundWebSocketDiscriminators[messageType.ToString()!] = schema.Reference.ReferenceV3;
}
var inboundWebSocketMessageSchema = new OpenApiSchema
{
Type = "object",
Description = "Represents the list of possible inbound websocket types",
Reference = new OpenApiReference
{
Id = nameof(InboundWebSocketMessage),
Type = ReferenceType.Schema
},
OneOf = inboundWebSocketSchemas,
Discriminator = new OpenApiDiscriminator
{
PropertyName = nameof(WebSocketMessage.MessageType),
Mapping = inboundWebSocketDiscriminators
}
};
context.SchemaRepository.AddDefinition(nameof(InboundWebSocketMessage), inboundWebSocketMessageSchema);
var outboundWebSocketSchemas = new List<OpenApiSchema>();
var outboundWebSocketDiscriminators = new Dictionary<string, string>();
foreach (var type in webSocketTypes.Where(t => typeof(IOutboundWebSocketMessage).IsAssignableFrom(t)))
{
var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute<DefaultValueAttribute>()?.Value;
if (messageType is null)
{
continue;
}
// Additional discriminator needed for GroupUpdate models...
if (messageType == SessionMessageType.SyncPlayGroupUpdate && type != typeof(SyncPlayGroupUpdateCommandMessage))
{
continue;
}
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
outboundWebSocketSchemas.Add(schema);
outboundWebSocketDiscriminators.Add(messageType.ToString()!, schema.Reference.ReferenceV3);
}
var outboundWebSocketMessageSchema = new OpenApiSchema
{
Type = "object",
Description = "Represents the list of possible outbound websocket types",
Reference = new OpenApiReference
{
Id = nameof(OutboundWebSocketMessage),
Type = ReferenceType.Schema
},
OneOf = outboundWebSocketSchemas,
Discriminator = new OpenApiDiscriminator
{
PropertyName = nameof(WebSocketMessage.MessageType),
Mapping = outboundWebSocketDiscriminators
}
};
context.SchemaRepository.AddDefinition(nameof(OutboundWebSocketMessage), outboundWebSocketMessageSchema);
context.SchemaRepository.AddDefinition(
nameof(WebSocketMessage),
new OpenApiSchema
{
Type = "object",
Description = "Represents the possible websocket types",
Reference = new OpenApiReference
{
Id = nameof(WebSocketMessage),
Type = ReferenceType.Schema
},
OneOf = new[]
{
inboundWebSocketMessageSchema,
outboundWebSocketMessageSchema
}
});
// Manually generate sync play GroupUpdate messages.
if (!context.SchemaRepository.Schemas.TryGetValue(nameof(GroupUpdate), out var groupUpdateSchema))
{
groupUpdateSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate), context.SchemaRepository);
}
var groupUpdateOfGroupInfoSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<GroupInfoDto>), context.SchemaRepository);
var groupUpdateOfGroupStateSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<GroupStateUpdate>), context.SchemaRepository);
var groupUpdateOfStringSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<string>), context.SchemaRepository);
var groupUpdateOfPlayQueueSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<PlayQueueUpdate>), context.SchemaRepository);
groupUpdateSchema.OneOf = new List<OpenApiSchema>
{
groupUpdateOfGroupInfoSchema,
groupUpdateOfGroupStateSchema,
groupUpdateOfStringSchema,
groupUpdateOfPlayQueueSchema
};
groupUpdateSchema.Discriminator = new OpenApiDiscriminator
{
PropertyName = nameof(GroupUpdate.Type),
Mapping = new Dictionary<string, string>
{
{ GroupUpdateType.UserJoined.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 },
{ GroupUpdateType.UserLeft.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 },
{ GroupUpdateType.GroupJoined.ToString(), groupUpdateOfGroupInfoSchema.Reference.ReferenceV3 },
{ GroupUpdateType.GroupLeft.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 },
{ GroupUpdateType.StateUpdate.ToString(), groupUpdateOfGroupStateSchema.Reference.ReferenceV3 },
{ GroupUpdateType.PlayQueue.ToString(), groupUpdateOfPlayQueueSchema.Reference.ReferenceV3 },
{ GroupUpdateType.NotInGroup.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 },
{ GroupUpdateType.GroupDoesNotExist.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 },
{ GroupUpdateType.LibraryAccessDenied.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 }
}
};
context.SchemaGenerator.GenerateSchema(typeof(ServerDiscoveryInfo), context.SchemaRepository);
foreach (var configuration in _serverConfigurationManager.GetConfigurationStores())
{
if (_ignoredConfigurations.IndexOf(configuration.ConfigurationType) != -1)
{
continue;
}
context.SchemaGenerator.GenerateSchema(configuration.ConfigurationType, context.SchemaRepository);
}
context.SchemaRepository.AddDefinition(nameof(TranscodeReason), new OpenApiSchema
{
Type = "string",
Enum = Enum.GetNames<TranscodeReason>()
.Select(e => new OpenApiString(e))
.Cast<IOpenApiAny>()
.ToArray()
});
}
}
}