From 350ada5253a218046e9853adbbbec9d7bb88e673 Mon Sep 17 00:00:00 2001 From: cypherpotato Date: Thu, 2 Jan 2025 17:44:13 -0300 Subject: [PATCH] fix route collision detector --- .../{CadenteHttpListener.cs => HttpHost.cs} | 39 +- .../Sisk.ManagedHttpListener.csproj | 14 + .../HtmlDocumentationExporter.cs | 352 ++++++++++++++++++ .../Sisk.Documenting.Html.csproj | 17 + extensions/Sisk.Documenting.Html/Style.cs | 160 ++++++++ .../Annotations/ApiEndpointAttribute.cs | 30 ++ .../Annotations/ApiHeaderAttribute.cs | 38 ++ .../Annotations/ApiParameterAttribute.cs | 46 +++ .../Annotations/ApiPathParameterAttribute.cs | 38 ++ .../Annotations/ApiRequestAttribute.cs | 38 ++ .../Annotations/ApiResponseAttribute.cs | 46 +++ .../Sisk.Documenting/ApiDocumentation.cs | 232 ++++++++++++ .../ApiDocumentationReader.cs | 101 +++++ extensions/Sisk.Documenting/ApiIdentifier.cs | 22 ++ .../IApiDocumentationExporter.cs | 14 + .../Sisk.Documenting.csproj} | 4 - .../ApiDefinitionAttribute.cs | 9 - .../ApiResponseAttribute.cs | 11 - .../Sisk.Helpers.Swagger/OpenApiGenerator.cs | 109 ------ .../MitmproxyProvider.cs | 8 +- .../Documentation/DocumentationDescriptor.cs | 4 +- .../Documentation/JsonRpcDocumentation.cs | 36 +- .../Documentation/JsonRpcHtmlExport.cs | 33 ++ .../Documentation/JsonRpcJsonExport.cs | 30 +- extensions/Sisk.JsonRPC/JsonRpcHandler.cs | 10 +- .../Sisk.JsonRPC/JsonRpcTransportLayer.cs | 17 - src/Http/Handlers/DefaultHttpServerHandler.cs | 4 +- src/Routing/Route.cs | 26 +- src/Routing/Router__CoreSetters.cs | 6 +- 29 files changed, 1294 insertions(+), 200 deletions(-) rename cadente/Sisk.Cadente/{CadenteHttpListener.cs => HttpHost.cs} (79%) create mode 100644 cadente/Sisk.Cadente/Sisk.ManagedHttpListener.csproj create mode 100644 extensions/Sisk.Documenting.Html/HtmlDocumentationExporter.cs create mode 100644 extensions/Sisk.Documenting.Html/Sisk.Documenting.Html.csproj create mode 100644 extensions/Sisk.Documenting.Html/Style.cs create mode 100644 extensions/Sisk.Documenting/Annotations/ApiEndpointAttribute.cs create mode 100644 extensions/Sisk.Documenting/Annotations/ApiHeaderAttribute.cs create mode 100644 extensions/Sisk.Documenting/Annotations/ApiParameterAttribute.cs create mode 100644 extensions/Sisk.Documenting/Annotations/ApiPathParameterAttribute.cs create mode 100644 extensions/Sisk.Documenting/Annotations/ApiRequestAttribute.cs create mode 100644 extensions/Sisk.Documenting/Annotations/ApiResponseAttribute.cs create mode 100644 extensions/Sisk.Documenting/ApiDocumentation.cs create mode 100644 extensions/Sisk.Documenting/ApiDocumentationReader.cs create mode 100644 extensions/Sisk.Documenting/ApiIdentifier.cs create mode 100644 extensions/Sisk.Documenting/IApiDocumentationExporter.cs rename extensions/{Sisk.Helpers.Swagger/Sisk.Helpers.Swagger.csproj => Sisk.Documenting/Sisk.Documenting.csproj} (75%) delete mode 100644 extensions/Sisk.Helpers.Swagger/ApiDefinitionAttribute.cs delete mode 100644 extensions/Sisk.Helpers.Swagger/ApiResponseAttribute.cs delete mode 100644 extensions/Sisk.Helpers.Swagger/OpenApiGenerator.cs diff --git a/cadente/Sisk.Cadente/CadenteHttpListener.cs b/cadente/Sisk.Cadente/HttpHost.cs similarity index 79% rename from cadente/Sisk.Cadente/CadenteHttpListener.cs rename to cadente/Sisk.Cadente/HttpHost.cs index 6afa23e..df5d612 100644 --- a/cadente/Sisk.Cadente/CadenteHttpListener.cs +++ b/cadente/Sisk.Cadente/HttpHost.cs @@ -4,7 +4,7 @@ // The code below is licensed under the MIT license as // of the date of its publication, available at // -// File name: CadenteHttpListener.cs +// File name: HttpHost.cs // Repository: https://github.com/sisk-http/core using System.Net; @@ -17,10 +17,9 @@ namespace Sisk.Cadente; /// /// Represents an HTTP host that listens for incoming TCP connections and handles HTTP requests. /// -public sealed class CadenteHttpListener : IDisposable { +public sealed class HttpHost : IDisposable { - // defines the connection queue size (worker connections) - const int QUEUE_SIZE = 512; + const int QUEUE_SIZE = 256; private readonly TcpListener _listener; private readonly Channel clientQueue; @@ -31,32 +30,32 @@ public sealed class CadenteHttpListener : IDisposable { private bool disposedValue; /// - /// Gets the action handler for processing HTTP actions. + /// Gets or sets the action handler for HTTP requests. /// public HttpAction ActionHandler { get; } /// - /// Gets a value indicating whether the host has been disposed. + /// Gets a value indicating whether this has been disposed. /// public bool IsDisposed { get => this.disposedValue; } /// - /// Gets or sets the port on which the HTTP host listens for incoming connections. - /// Default is 8080. + /// Gets or sets the port number to listen on. /// public int Port { get; set; } = 8080; /// - /// Gets or sets the options for HTTPS configuration. + /// Gets or sets the HTTPS options for secure connections. Setting an object in this + /// property, the will use HTTPS instead of HTTP. /// public HttpsOptions? HttpsOptions { get; set; } /// - /// Initializes a new instance of the class with the specified port and action handler. + /// Initializes a new instance of the class. /// - /// The port on which the host will listen for incoming connections. - /// The action handler for processing HTTP actions. - public CadenteHttpListener ( int port, HttpAction actionHandler ) { + /// The port number to listen on. + /// The action handler for HTTP requests. + public HttpHost ( int port, HttpAction actionHandler ) { this._listener = new TcpListener ( new IPEndPoint ( IPAddress.Any, port ) ); this.channelConsumerThread = new Thread ( this.ConsumerJobThread ); this.clientQueue = Channel.CreateBounded ( @@ -67,12 +66,12 @@ public CadenteHttpListener ( int port, HttpAction actionHandler ) { } /// - /// Starts the Cadente HTTP host, beginning to listen for incoming connections. + /// Starts the HTTP host and begins listening for incoming connections. /// public void Start () { ObjectDisposedException.ThrowIf ( this.disposedValue, this ); - this._listener.Server.SetSocketOption ( SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, 3 ); + this._listener.Server.SetSocketOption ( SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, 1 ); this._listener.Server.SetSocketOption ( SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, 120 ); this._listener.Server.SetSocketOption ( SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, 3 ); this._listener.Server.SetSocketOption ( SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true ); @@ -96,8 +95,8 @@ private async Task HandleTcpClient ( TcpClient client ) { { // setup the tcpclient client.NoDelay = true; - client.ReceiveTimeout = 5_000; - client.SendTimeout = 5_000; + client.ReceiveTimeout = (int) TimeSpan.FromSeconds ( 5 ).TotalMilliseconds; + client.SendTimeout = (int) TimeSpan.FromSeconds ( 5 ).TotalMilliseconds; client.ReceiveBufferSize = HttpConnection.REQUEST_BUFFER_SIZE; client.SendBufferSize = HttpConnection.RESPONSE_BUFFER_SIZE; @@ -126,14 +125,16 @@ await sslStream.AuthenticateAsServerAsync ( checkCertificateRevocation: this.HttpsOptions.CheckCertificateRevocation, enabledSslProtocols: this.HttpsOptions.AllowedProtocols ); } - catch (Exception) { + catch (Exception ex) { //Logger.LogInformation ( $"[{connection.Id}] Failed SSL authenticate: {ex.Message}" ); - // TODO drop connection with 401 } } + //Logger.LogInformation ( $"[{connection.Id}] Begin handle connection" ); var state = await connection.HandleConnectionEvents (); + //Logger.LogInformation ( $"[{connection.Id}] Ended handling connection with state {state}" ); + } } finally { diff --git a/cadente/Sisk.Cadente/Sisk.ManagedHttpListener.csproj b/cadente/Sisk.Cadente/Sisk.ManagedHttpListener.csproj new file mode 100644 index 0000000..6344ee1 --- /dev/null +++ b/cadente/Sisk.Cadente/Sisk.ManagedHttpListener.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + true + + + + $(DefineConstants);VERBOSE + + + diff --git a/extensions/Sisk.Documenting.Html/HtmlDocumentationExporter.cs b/extensions/Sisk.Documenting.Html/HtmlDocumentationExporter.cs new file mode 100644 index 0000000..ea4e163 --- /dev/null +++ b/extensions/Sisk.Documenting.Html/HtmlDocumentationExporter.cs @@ -0,0 +1,352 @@ + +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Sisk.Core.Http; +using Sisk.Core.Routing; +using TinyComponents; + +namespace Sisk.Documenting.Html; + +/// +/// Represents a class for exporting API documentation to HTML format. +/// +public class HtmlDocumentationExporter : IApiDocumentationExporter { + + /// + /// Gets or sets the title of the HTML page. + /// + public string PageTitle { get; set; } = "API documentation"; + + /// + /// Gets or sets the CSS styles of the HTML page. + /// + public string Style { get; set; } = Html.Style.DefaultStyles; + + /// + /// Gets or sets the JavaScript script to be included in the HTML page. + /// + public string? Script { get; set; } + + /// + /// Gets or sets the format string for the main title service version. + /// + public string FormatMainTitleServiceVersion { get; set; } = "Service version: {0}"; + + /// + /// Gets or sets the format string for endpoint headers. + /// + public string FormatEndpointHeaders { get; set; } = "Headers:"; + + /// + /// Gets or sets the format string for endpoint path parameters. + /// + public string FormatEndpointPathParameters { get; set; } = "Path parameters:"; + + /// + /// Gets or sets the format string for endpoint request parameters. + /// + public string FormatEndpointParameters { get; set; } = "Request parameters:"; + + /// + /// Gets or sets the format string for endpoint responses. + /// + public string FormatEndpointResponses { get; set; } = "Responses:"; + + /// + /// Gets or sets the format string for endpoint request examples. + /// + public string FormatEndpointRequestExamples { get; set; } = "Request examples:"; + + /// + /// Gets or sets the format string for required text. + /// + public string FormatRequiredText { get; set; } = "Required"; + + /// + /// Writes the main title of the API documentation. + /// + /// The API documentation to write the title for. + /// The HTML element representing the main title. + protected virtual HtmlElement WriteMainTitle ( ApiDocumentation documentation ) { + return HtmlElement.Fragment ( fragment => { + fragment += new HtmlElement ( "h1", documentation.ApplicationName ?? "Application name" ) + .WithClass ( "app-title" ); + fragment += this.CreateParagraphs ( documentation.ApplicationDescription ); + fragment += new HtmlElement ( "p", string.Format ( this.FormatMainTitleServiceVersion, documentation.ApiVersion ?? "1.0" ) ); + } ); + } + + /// + /// Writes the description of an API endpoint. + /// + /// The API endpoint to write the description for. + /// The HTML element representing the endpoint description, or null if no description is available. + protected virtual HtmlElement? WriteEndpointDescription ( ApiEndpoint endpoint ) { + return new HtmlElement ( "details", details => { + details.ClassList.Add ( "endpoint-description" ); + + details += new HtmlElement ( "summary", summary => { + summary.Id = this.TransformId ( endpoint.Name ); + + summary += new HtmlElement ( "span", endpoint.RouteMethod ).WithStyle ( new { backgroundColor = this.GetRouteMethodHexColor ( endpoint.RouteMethod ) + "40" } ); + summary += new HtmlElement ( "span", endpoint.Path ); + summary += new HtmlElement ( "span", $" - {endpoint.Name}" ).WithClass ( "muted" ); + } ); + + details += new HtmlElement ( "h3", endpoint.Name ); + details += this.CreateParagraphs ( endpoint.Description ); + + if (endpoint.Headers.Length > 0) { + details += new HtmlElement ( "div", div => { + div += new HtmlElement ( "p", this.FormatEndpointHeaders ); + + div += new HtmlElement ( "ul", ul => { + foreach (var header in endpoint.Headers) { + ul += new HtmlElement ( "li", li => { + li.ClassList.Add ( "item-description" ); + + li += new HtmlElement ( "code", header.HeaderName ); + li += new HtmlElement ( "span", header.IsRequired ? this.FormatRequiredText : "" ).WithClass ( "at", "ml3" ); + li += new HtmlElement ( "div", this.CreateParagraphs ( header.Description ) ); + } ); + } + } ); + } ); + } + + if (endpoint.PathParameters.Length > 0) { + details += new HtmlElement ( "div", div => { + div += new HtmlElement ( "p", this.FormatEndpointPathParameters ); + + div += new HtmlElement ( "ul", ul => { + foreach (var pathParam in endpoint.PathParameters) { + ul += new HtmlElement ( "li", li => { + li.ClassList.Add ( "item-description" ); + + li += new HtmlElement ( "code", pathParam.Name ); + li += new HtmlElement ( "span", pathParam.Type ).WithClass ( "muted", "ml3" ); + li += new HtmlElement ( "div", this.CreateParagraphs ( pathParam.Description ) ); + } ); + } + } ); + } ); + } + + if (endpoint.Parameters.Length > 0) { + details += new HtmlElement ( "div", div => { + div += new HtmlElement ( "p", this.FormatEndpointParameters ); + + div += new HtmlElement ( "ul", ul => { + foreach (var param in endpoint.Parameters) { + ul += new HtmlElement ( "li", li => { + li.ClassList.Add ( "item-description" ); + + li += new HtmlElement ( "code", param.Name ); + li += new HtmlElement ( "span", param.TypeName ).WithClass ( "muted", "ml3" ); + li += new HtmlElement ( "span", param.IsRequired ? this.FormatRequiredText : "" ).WithClass ( "at", "ml3" ); + li += new HtmlElement ( "div", this.CreateParagraphs ( param.Description ) ); + } ); + } + } ); + } ); + } + + if (endpoint.RequestExamples.Length > 0) { + details += new HtmlElement ( "div", div => { + div += new HtmlElement ( "p", this.FormatEndpointRequestExamples ); + + div += new HtmlElement ( "ul", ul => { + foreach (var req in endpoint.RequestExamples) { + ul += new HtmlElement ( "li", li => { + li.ClassList.Add ( "item-description" ); + + li += this.CreateParagraphs ( req.Description ); + + if (req.Example != null) { + li += new HtmlElement ( "div", exampleDiv => { + exampleDiv += this.CreateCodeBlock ( req.Example, req.ExampleLanguage ); + } ); + } + } ); + } + } ); + } ); + } + + if (endpoint.Responses.Length > 0) { + details += new HtmlElement ( "div", div => { + div += new HtmlElement ( "p", this.FormatEndpointResponses ); + + div += new HtmlElement ( "ul", ul => { + foreach (var res in endpoint.Responses) { + ul += new HtmlElement ( "li", li => { + li.ClassList.Add ( "item-description" ); + + li += new HtmlElement ( "code", (int) res.StatusCode ); + li += new HtmlElement ( "div", this.CreateParagraphs ( res.Description ) ); + + if (res.Example != null) { + li += new HtmlElement ( "div", exampleDiv => { + exampleDiv += this.CreateCodeBlock ( res.Example, res.ExampleLanguage ); + } ); + } + } ); + } + } ); + } ); + } + } ); + } + + /// + /// Creates an HTML code block element from the provided code and language. + /// + /// The code to display in the code block. + /// The programming language of the code, or null for no language highlighting. + /// The HTML element representing the code block, or null if no code is provided. + protected virtual HtmlElement? CreateCodeBlock ( string code, string? language ) { + return new HtmlElement ( "pre", pre => { + pre += new HtmlElement ( "code", cblock => { + if (language != null) { + cblock.ClassList.Add ( $"language-{language}" ); + cblock.ClassList.Add ( $"lang-{language}" ); + } + cblock += code; + } ); + } ); + } + + /// + /// Creates an HTML badge element for an API endpoint. + /// + /// The HTTP method of the endpoint (e.g. GET, POST, PUT, etc.). + /// The path of the endpoint, or null for no path display. + /// The HTML element representing the endpoint badge, or null if no badge is applicable. + protected virtual HtmlElement? CreateEndpointBadge ( RouteMethod method, string? path ) { + + string spanColor = this.GetRouteMethodHexColor ( method ); + + return new HtmlElement ( "span22", span => { + span.ClassList.Add ( "endpoint-badge" ); + span += new HtmlElement ( "span", method ).WithStyle ( new { backgroundColor = $"{spanColor}43" } ); + span += new HtmlElement ( "span", path ); + } ); + } + + /// + /// Creates one or more HTML paragraph elements from the provided text. + /// + /// The text to display in the paragraphs, or null for no paragraphs. + /// The HTML element representing the paragraphs, or null if no text is provided. + [return: NotNullIfNotNull ( nameof ( text ) )] + protected virtual HtmlElement? CreateParagraphs ( string? text ) { + if (text is null) + return null; + + return HtmlElement.Fragment ( fragment => { + foreach (var s in text.Split ( '\n', StringSplitOptions.RemoveEmptyEntries )) { + fragment += new HtmlElement ( "p", s ); + } + } ); + } + + /// + /// Gets the hex color code associated with the specified route method. + /// + /// The route method to get the color for. + /// The hex color code as a string. + protected virtual string GetRouteMethodHexColor ( RouteMethod rm ) { + return rm switch { + RouteMethod.Get => "#10f25f", + RouteMethod.Post => "#f26710", + RouteMethod.Put => "#3210f2", + RouteMethod.Patch => "#6319c4", + RouteMethod.Delete => "#c41919", + _ => "#549696" + }; + } + + /// + /// Transforms an unsafe ID into a safe and valid HTML ID. + /// + /// The ID to transform, which may contain invalid characters. + /// The transformed ID, which is safe for use in HTML. + protected string TransformId ( string unsafeId ) { + return new string ( unsafeId.Where ( char.IsLetterOrDigit ).ToArray () ); + } + + /// + /// Truncates a string to the specified size, appending an ellipsis if necessary. + /// + /// The string to truncate, or null for no truncation. + /// The maximum length of the string, including the ellipsis. + /// The truncated string, or the original string if it is already within the size limit, or null if the input string is null. + [return: NotNullIfNotNull ( nameof ( s ) )] + protected string? Ellipsis ( string? s, int size ) { + if (string.IsNullOrEmpty ( s )) { + return s; + } + if (s.Length <= size) { + return s; + } + if (size <= 3) { + return s.Substring ( 0, size ) + "..."; + } + return s.Substring ( 0, size - 3 ) + "..."; + } + + /// + /// Exports the API documentation as an HTML string. + /// + /// The API documentation to export. + /// The exported HTML string. + public string ExportHtml ( ApiDocumentation d ) { + HtmlElement html = new HtmlElement ( "html" ); + + html += new HtmlElement ( "head", head => { + head += new HtmlElement ( "meta" ) + .WithAttribute ( "charset", "UTF-8" ) + .SelfClosed (); + head += new HtmlElement ( "meta" ) + .WithAttribute ( "name", "viewport" ) + .WithAttribute ( "content", "width=device-width, initial-scale=1.0" ) + .SelfClosed (); + + head += new HtmlElement ( "title", this.PageTitle ); + head += new HtmlElement ( "style", RenderableText.Raw ( this.Style ) ); + head += new HtmlElement ( "script", RenderableText.Raw ( this.Script ) ); + } ); + + html += new HtmlElement ( "body", body => { + body += new HtmlElement ( "main", main => { + + main += this.WriteMainTitle ( d ); + + var groups = d.Endpoints.GroupBy ( e => e.Group ); + foreach (var item in groups) { + + main += new HtmlElement ( "h2", $"{item.Key}:" ); + + foreach (var endpoint in item.OrderBy ( i => i.Path.Length )) { + main += new HtmlElement ( "section", section => { + section.ClassList.Add ( "endpoint" ); + + section += this.WriteEndpointDescription ( endpoint ); + } ); + } + } + } ); + } ); + + return html.ToString (); + } + + /// + /// Exports the API documentation as HTTP content. + /// + /// The API documentation to export. + /// The exported API documentation as HTTP content. + public HttpContent ExportDocumentationContent ( ApiDocumentation documentation ) { + return new HtmlContent ( this.ExportHtml ( documentation ), Encoding.UTF8 ); + } +} diff --git a/extensions/Sisk.Documenting.Html/Sisk.Documenting.Html.csproj b/extensions/Sisk.Documenting.Html/Sisk.Documenting.Html.csproj new file mode 100644 index 0000000..2b1442a --- /dev/null +++ b/extensions/Sisk.Documenting.Html/Sisk.Documenting.Html.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/extensions/Sisk.Documenting.Html/Style.cs b/extensions/Sisk.Documenting.Html/Style.cs new file mode 100644 index 0000000..2deac2d --- /dev/null +++ b/extensions/Sisk.Documenting.Html/Style.cs @@ -0,0 +1,160 @@ +namespace Sisk.Documenting.Html; +internal class Style { + public const string DefaultStyles = +""" +* { box-sizing: border-box; } +p, li { line-height: 1.6 } + +:root { + --font-monospace: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; +} + +html, body { + margin: 0; + background-color: white; + font-size: 16px; + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji" +} + +main { + background: white; + max-width: 1200px; + width: 90vw; + margin: 30px auto 0 auto; + padding: 20px 40px; + border-radius: 14px; + border: 1px solid #d1d9e0; +} + +h1, h2, h3 { + margin-top: 2.5rem; + margin-bottom: 1rem; + font-weight: 600; + line-height: 1.25; +} + +h1, h2 { + padding-bottom: .3em; + border-bottom: 2px solid #d1d9e0b3; +} + +h1 { + font-size: 2em; +} + +h2 { + font-size: 1.5em; +} + +h1 a, +h2 a { + opacity: 0; + color: #000; + text-decoration: none !important; + user-select: none; +} + +h1:hover a, +h2:hover a { + opacity: 0.3; +} + +h1 a:hover, +h2 a:hover { + opacity: 1; +} + +hr { + border-top: none; + border-bottom: 3px solid #d1d9e0; +} + +pre { + background-color: #f6f8fa; + color: black; + padding: 1rem; + overflow: auto; + font-size: 85%; + line-height: 1.45; + border-radius: 6px; +} + +.muted { + color: #656d76; +} + +.at { + color: #9a6700; +} + +a { + color: #0969da; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.endpoint-description > summary { + display: block; + width: 100%; + padding: .5em; + border-radius: 6px; + border: 1px solid #d1d9e0; + cursor: pointer; +} + +.endpoint-description > summary:hover { + background-color: #f6f8fa; +} + +.endpoint-description > summary > span:nth-child(1) { + display: inline-block; + padding: 4px 0; + width: 70px; + text-align: center; + border-radius: 3px; + font-weight: 700; + font-size: .8em; + color: #000000bb; + text-transform: uppercase; + user-select: none; +} + +.endpoint-description > summary > span:nth-child(2) { + font-family: var(--font-monospace); + font-size: 14px; + margin-left: 10px; +} + +.endpoint-description > summary > span:nth-child(3) { + user-select: none; +} + +.endpoint-description h3 { + margin: 1em 0 1.25em 0; +} + +.endpoint + .endpoint { + margin-top: .25em; +} + +:not(pre) > code { + background-color: #eff1f3; + padding: .2em .4em; + font-family: var(--font-monospace); + font-size: 14px; + border-radius: 4px; +} + +.item-description + .item-description { + padding-top: 1em;uj + border-top: 1px solid #d1d9e0; +} + +.ml1 { margin-left: .25em; } +.ml2 { margin-left: .5em; } +.ml3 { margin-left: .75em; } +"""; +} diff --git a/extensions/Sisk.Documenting/Annotations/ApiEndpointAttribute.cs b/extensions/Sisk.Documenting/Annotations/ApiEndpointAttribute.cs new file mode 100644 index 0000000..e89af4e --- /dev/null +++ b/extensions/Sisk.Documenting/Annotations/ApiEndpointAttribute.cs @@ -0,0 +1,30 @@ +namespace Sisk.Documenting.Annotations; + +/// +/// Specifies an attribute for an API endpoint, allowing metadata such as name, description, and group to be associated with methods. +/// +[AttributeUsage ( AttributeTargets.Method, AllowMultiple = false )] +public sealed class ApiEndpointAttribute : Attribute { + /// + /// Gets or sets the name of the API endpoint. + /// + public string Name { get; set; } + + /// + /// Gets or sets the description of the API endpoint. + /// + public string? Description { get; set; } + + /// + /// Gets or sets the group to which the API endpoint belongs. + /// + public string? Group { get; set; } + + /// + /// Initializes a new instance of the class with the specified endpoint name. + /// + /// The name of the API endpoint. + public ApiEndpointAttribute ( string name ) { + this.Name = name; + } +} diff --git a/extensions/Sisk.Documenting/Annotations/ApiHeaderAttribute.cs b/extensions/Sisk.Documenting/Annotations/ApiHeaderAttribute.cs new file mode 100644 index 0000000..49ebabe --- /dev/null +++ b/extensions/Sisk.Documenting/Annotations/ApiHeaderAttribute.cs @@ -0,0 +1,38 @@ +namespace Sisk.Documenting.Annotations; + +/// +/// Specifies an attribute for an API header, allowing metadata such as header name, description, and requirement status to be associated with methods. +/// +[AttributeUsage ( AttributeTargets.Method, AllowMultiple = true )] +public sealed class ApiHeaderAttribute : Attribute { + /// + /// Gets the name of the header. + /// + public string HeaderName { get; } + + /// + /// Gets or sets the description of the header. + /// + public string? Description { get; set; } + + /// + /// Gets or sets a value indicating whether the header is required. + /// + public bool IsRequired { get; set; } + + /// + /// Initializes a new instance of the class with the specified header name. + /// + /// The name of the header. + public ApiHeaderAttribute ( string headerName ) { + this.HeaderName = headerName; + } + + internal ApiEndpointHeader GetApiEndpointObject () { + return new ApiEndpointHeader { + HeaderName = this.HeaderName, + Description = this.Description, + IsRequired = this.IsRequired + }; + } +} diff --git a/extensions/Sisk.Documenting/Annotations/ApiParameterAttribute.cs b/extensions/Sisk.Documenting/Annotations/ApiParameterAttribute.cs new file mode 100644 index 0000000..508333e --- /dev/null +++ b/extensions/Sisk.Documenting/Annotations/ApiParameterAttribute.cs @@ -0,0 +1,46 @@ +namespace Sisk.Documenting.Annotations; + +/// +/// Specifies an attribute for an API parameter, allowing metadata such as name, type, description, and requirement status to be associated with methods. +/// +[AttributeUsage ( AttributeTargets.Method, AllowMultiple = true )] +public sealed class ApiParameterAttribute : Attribute { + /// + /// Gets the name of the parameter. + /// + public string Name { get; } + + /// + /// Gets the type name of the parameter. + /// + public string TypeName { get; } + + /// + /// Gets or sets the description of the parameter. + /// + public string? Description { get; set; } + + /// + /// Gets or sets a value indicating whether the parameter is required. + /// + public bool IsRequired { get; set; } + + /// + /// Initializes a new instance of the class with the specified name and type name. + /// + /// The name of the parameter. + /// The type name of the parameter. + public ApiParameterAttribute ( string name, string typeName ) { + this.Name = name; + this.TypeName = typeName; + } + + internal ApiEndpointParameter GetApiEndpointObject () { + return new ApiEndpointParameter { + Name = this.Name, + TypeName = this.TypeName, + Description = this.Description, + IsRequired = this.IsRequired + }; + } +} diff --git a/extensions/Sisk.Documenting/Annotations/ApiPathParameterAttribute.cs b/extensions/Sisk.Documenting/Annotations/ApiPathParameterAttribute.cs new file mode 100644 index 0000000..3dda7f2 --- /dev/null +++ b/extensions/Sisk.Documenting/Annotations/ApiPathParameterAttribute.cs @@ -0,0 +1,38 @@ +namespace Sisk.Documenting.Annotations; + +/// +/// Specifies an attribute for an API path parameter, allowing metadata such as name, type, and description to be associated with methods. +/// +[AttributeUsage ( AttributeTargets.Method, AllowMultiple = true )] +public sealed class ApiPathParameterAttribute : Attribute { + /// + /// Gets the name of the path parameter. + /// + public string Name { get; } + + /// + /// Gets or sets the type of the path parameter. + /// + public string? Type { get; set; } + + /// + /// Gets or sets the description of the path parameter. + /// + public string? Description { get; set; } + + /// + /// Initializes a new instance of the class with the specified name. + /// + /// The name of the path parameter. + public ApiPathParameterAttribute ( string name ) { + this.Name = name; + } + + internal ApiEndpointPathParameter GetApiEndpointObject () { + return new ApiEndpointPathParameter { + Name = this.Name, + Description = this.Description, + Type = this.Type + }; + } +} diff --git a/extensions/Sisk.Documenting/Annotations/ApiRequestAttribute.cs b/extensions/Sisk.Documenting/Annotations/ApiRequestAttribute.cs new file mode 100644 index 0000000..2badb47 --- /dev/null +++ b/extensions/Sisk.Documenting/Annotations/ApiRequestAttribute.cs @@ -0,0 +1,38 @@ +namespace Sisk.Documenting.Annotations; + +/// +/// Specifies an attribute for an API request, allowing metadata such as description, example language, and example content to be associated with methods. +/// +[AttributeUsage ( AttributeTargets.Method, AllowMultiple = true )] +public sealed class ApiRequestAttribute : Attribute { + /// + /// Gets or sets the description of the API request. + /// + public string Description { get; set; } + + /// + /// Gets or sets the programming language used in the example, if applicable. + /// + public string? ExampleLanguage { get; set; } + + /// + /// Gets or sets the actual example request content. + /// + public string? Example { get; set; } + + /// + /// Initializes a new instance of the class with the specified description. + /// + /// The description of the API request. + public ApiRequestAttribute ( string description ) { + this.Description = description; + } + + internal ApiEndpointRequestExample GetApiEndpointObject () { + return new ApiEndpointRequestExample { + Description = this.Description, + ExampleLanguage = this.ExampleLanguage, + Example = this.Example, + }; + } +} diff --git a/extensions/Sisk.Documenting/Annotations/ApiResponseAttribute.cs b/extensions/Sisk.Documenting/Annotations/ApiResponseAttribute.cs new file mode 100644 index 0000000..adeb0f6 --- /dev/null +++ b/extensions/Sisk.Documenting/Annotations/ApiResponseAttribute.cs @@ -0,0 +1,46 @@ +using System.Net; + +namespace Sisk.Documenting.Annotations; + +/// +/// Specifies an attribute for an API response, allowing metadata such as status code, description, example content, and example language to be associated with methods. +/// +[AttributeUsage ( AttributeTargets.Method, AllowMultiple = true )] +public sealed class ApiResponseAttribute : Attribute { + /// + /// Gets the HTTP status code for the response. + /// + public HttpStatusCode StatusCode { get; } + + /// + /// Gets or sets the description of the response. + /// + public string? Description { get; set; } + + /// + /// Gets or sets the example response content. + /// + public string? Example { get; set; } + + /// + /// Gets or sets the programming language used in the example, if applicable. + /// + public string? ExampleLanguage { get; set; } + + /// + /// Initializes a new instance of the class with the specified status code. + /// + /// The HTTP status code for the response. + public ApiResponseAttribute ( HttpStatusCode statusCode ) { + this.StatusCode = statusCode; + } + + internal ApiEndpointResponse GetApiEndpointObject () { + return new ApiEndpointResponse { + StatusCode = this.StatusCode, + Description = this.Description, + Example = this.Example, + ExampleLanguage = this.ExampleLanguage, + }; + } +} diff --git a/extensions/Sisk.Documenting/ApiDocumentation.cs b/extensions/Sisk.Documenting/ApiDocumentation.cs new file mode 100644 index 0000000..6eb1f7d --- /dev/null +++ b/extensions/Sisk.Documenting/ApiDocumentation.cs @@ -0,0 +1,232 @@ +using System.Net; +using Sisk.Core.Routing; + +namespace Sisk.Documenting; + +/// +/// Represents the API documentation, including application details and endpoints. +/// +public sealed class ApiDocumentation { + + /// + /// Gets or sets the name of the application. + /// + public string? ApplicationName { get; internal set; } + + /// + /// Gets or sets the description of the application. + /// + public string? ApplicationDescription { get; internal set; } + + /// + /// Gets or sets the version of the API. + /// + public string? ApiVersion { get; internal set; } + + /// + /// Gets or sets the array of API endpoints. + /// + public ApiEndpoint [] Endpoints { get; internal set; } = null!; + + /// + /// Generates an instance of by reading documentation from the specified router and identifier. + /// + /// The router used to generate the documentation. + /// The identifier for the API documentation. + /// An instance of . + public static ApiDocumentation Generate ( Router router, ApiIdentifier identifier ) { + return ApiDocumentationReader.ReadDocumentation ( identifier, router ); + } + + internal ApiDocumentation () { + } +} + +/// +/// Represents an API endpoint, including its metadata, request and response details. +/// +public sealed class ApiEndpoint { + + /// + /// Gets the name of the API endpoint. + /// + public string Name { get; internal set; } = null!; + + /// + /// Gets the description of the API endpoint. + /// + public string? Description { get; internal set; } + + /// + /// Gets the group to which the API endpoint belongs. + /// + public string? Group { get; internal set; } + + /// + /// Gets the route method used for the API endpoint. + /// + public RouteMethod RouteMethod { get; internal set; } + + /// + /// Gets the headers associated with the API endpoint. + /// + public ApiEndpointHeader [] Headers { get; internal set; } = null!; + + /// + /// Gets the parameters accepted by the API endpoint. + /// + public ApiEndpointParameter [] Parameters { get; internal set; } = null!; + + /// + /// Gets the parameters accepted by the API endpoint. + /// + public ApiEndpointRequestExample [] RequestExamples { get; internal set; } = null!; + + /// + /// Gets the possible responses from the API endpoint. + /// + public ApiEndpointResponse [] Responses { get; internal set; } = null!; + + /// + /// Gets the path parameters for the API endpoint. + /// + public ApiEndpointPathParameter [] PathParameters { get; internal set; } = null!; + + /// + /// Gets the path of the API endpoint. + /// + public string Path { get; internal set; } = null!; + + internal ApiEndpoint () { + } +} + +/// +/// Represents an example request for an API endpoint, including its description and example content. +/// +public sealed class ApiEndpointRequestExample { + + /// + /// Gets the description of the request example. + /// + public string Description { get; internal set; } = null!; + + /// + /// Gets the programming language used in the example, if applicable. + /// + public string? ExampleLanguage { get; internal set; } + + /// + /// Gets the actual example request content. + /// + public string? Example { get; internal set; } + + internal ApiEndpointRequestExample () { + } +} + +/// +/// Represents a path parameter for an API endpoint, including its name, type, and description. +/// +public sealed class ApiEndpointPathParameter { + + /// + /// Gets the name of the path parameter. + /// + public string Name { get; internal set; } = null!; + + /// + /// Gets the type of the path parameter. + /// + public string? Type { get; internal set; } + + /// + /// Gets the description of the path parameter. + /// + public string? Description { get; internal set; } + + internal ApiEndpointPathParameter () { + } +} + +/// +/// Represents a parameter for an API endpoint, including its name, type, and requirements. +/// +public sealed class ApiEndpointParameter { + + /// + /// Gets the name of the parameter. + /// + public string Name { get; internal set; } = null!; + + /// + /// Gets the type name of the parameter. + /// + public string TypeName { get; internal set; } = null!; + + /// + /// Gets the description of the parameter. + /// + public string? Description { get; internal set; } + + /// + /// Gets a value indicating whether the parameter is required. + /// + public bool IsRequired { get; internal set; } + + internal ApiEndpointParameter () { + } +} + +/// +/// Represents a response for an API endpoint, including the status code and example content. +/// +public sealed class ApiEndpointResponse { + + /// + /// Gets the HTTP status code for the response. + /// + public HttpStatusCode StatusCode { get; internal set; } + + /// + /// Gets the description of the response. + /// + public string? Description { get; internal set; } + + /// + /// Gets the example response content. + /// + public string? Example { get; internal set; } + + /// + /// Gets the programming language used in the example, if applicable. + /// + public string? ExampleLanguage { get; internal set; } + + internal ApiEndpointResponse () { + } +} + +/// +/// Represents a header for an API endpoint, including its name and requirements. +/// +public sealed class ApiEndpointHeader { + + /// + /// Gets or sets the name of the header. + /// + public string HeaderName { get; internal set; } = null!; + + /// + /// Gets or sets the description of the header. + /// + public string? Description { get; internal set; } + + /// + /// Gets or sets a value indicating whether the header is required. + /// + public bool IsRequired { get; internal set; } + + internal ApiEndpointHeader () { + } +} \ No newline at end of file diff --git a/extensions/Sisk.Documenting/ApiDocumentationReader.cs b/extensions/Sisk.Documenting/ApiDocumentationReader.cs new file mode 100644 index 0000000..9f2359a --- /dev/null +++ b/extensions/Sisk.Documenting/ApiDocumentationReader.cs @@ -0,0 +1,101 @@ +using System.Reflection; +using Sisk.Core.Routing; +using Sisk.Documenting.Annotations; + +namespace Sisk.Documenting; + +internal class ApiDocumentationReader { + + public static ApiDocumentation ReadDocumentation ( ApiIdentifier identifier, Router router ) { + var routes = router.GetDefinedRoutes (); + + List endpoints = new ( routes.Length ); + + foreach (var route in routes) { + var routeMethod = route.Action?.Method; + if (routeMethod is null) + continue; + + var apiEndpointAttr = routeMethod.GetCustomAttribute (); + if (apiEndpointAttr is null) + continue; + + List responses = new List (); + List parameters = new List (); + List headers = new List (); + List pathParameters = new List (); + List requests = new List (); + + foreach (var requestHandler in route.RequestHandlers) { + MethodInfo? rhMethod = ExtractRhExecute ( requestHandler ); + if (rhMethod is null) + continue; + + var rhAttrs = ExtractAttributesFromMethod ( rhMethod ); + foreach (var apiResAttr in rhAttrs.Item1) + responses.Add ( apiResAttr.GetApiEndpointObject () ); + foreach (var apiParamAttr in rhAttrs.Item2) + parameters.Add ( apiParamAttr.GetApiEndpointObject () ); + foreach (var apiHeaderAttr in rhAttrs.Item3) + headers.Add ( apiHeaderAttr.GetApiEndpointObject () ); + foreach (var apiPathParam in rhAttrs.Item4) + pathParameters.Add ( apiPathParam.GetApiEndpointObject () ); + foreach (var apiReq in rhAttrs.Item5) + requests.Add ( apiReq.GetApiEndpointObject () ); + } + + var attrs = ExtractAttributesFromMethod ( routeMethod ); + foreach (var apiResAttr in attrs.Item1) + responses.Add ( apiResAttr.GetApiEndpointObject () ); + foreach (var apiParamAttr in attrs.Item2) + parameters.Add ( apiParamAttr.GetApiEndpointObject () ); + foreach (var apiHeaderAttr in attrs.Item3) + headers.Add ( apiHeaderAttr.GetApiEndpointObject () ); + foreach (var apiPathParam in attrs.Item4) + pathParameters.Add ( apiPathParam.GetApiEndpointObject () ); + foreach (var apiReqParam in attrs.Item5) + requests.Add ( apiReqParam.GetApiEndpointObject () ); + + ApiEndpoint endpoint = new ApiEndpoint () { + Description = apiEndpointAttr.Description, + Group = apiEndpointAttr.Group, + Name = apiEndpointAttr.Name, + Path = route.Path, + RouteMethod = route.Method, + Headers = headers.ToArray (), + Parameters = parameters.ToArray (), + Responses = responses.ToArray (), + PathParameters = pathParameters.ToArray (), + RequestExamples = requests.ToArray () + }; + + endpoints.Add ( endpoint ); + } + + return new ApiDocumentation () { + ApiVersion = identifier.ApplicationVersion, + ApplicationDescription = identifier.ApplicationDescription, + ApplicationName = identifier.ApplicationName, + Endpoints = endpoints.ToArray (), + }; + } + + static (ApiResponseAttribute [], ApiParameterAttribute [], ApiHeaderAttribute [], ApiPathParameterAttribute [], ApiRequestAttribute []) ExtractAttributesFromMethod ( MethodInfo method ) { + var apiResponsesAttrs = method.GetCustomAttributes ().ToArray (); + var apiParametersAttrs = method.GetCustomAttributes ().ToArray (); + var apiHeadersAttrs = method.GetCustomAttributes ().ToArray (); + var apiPathParamsAttrs = method.GetCustomAttributes ().ToArray (); + var apiRequestsAttrs = method.GetCustomAttributes ().ToArray (); + return (apiResponsesAttrs, apiParametersAttrs, apiHeadersAttrs, apiPathParamsAttrs, apiRequestsAttrs); + } + + static MethodInfo? ExtractRhExecute ( IRequestHandler rh ) { + var rhType = rh.GetType (); + if (rh is AsyncRequestHandler) { + return rhType.GetMethod ( "ExecuteAsync", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ); + } + else { + return rhType.GetMethod ( "Execute", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ); + } + } +} diff --git a/extensions/Sisk.Documenting/ApiIdentifier.cs b/extensions/Sisk.Documenting/ApiIdentifier.cs new file mode 100644 index 0000000..31a3f6d --- /dev/null +++ b/extensions/Sisk.Documenting/ApiIdentifier.cs @@ -0,0 +1,22 @@ +namespace Sisk.Documenting; + +/// +/// Represents an identifier for an API, including application details such as name, version, and description. +/// +public sealed class ApiIdentifier { + + /// + /// Gets or sets the name of the application. + /// + public string? ApplicationName { get; set; } + + /// + /// Gets or sets the version of the application. + /// + public string? ApplicationVersion { get; set; } + + /// + /// Gets or sets the description of the application. + /// + public string? ApplicationDescription { get; set; } +} diff --git a/extensions/Sisk.Documenting/IApiDocumentationExporter.cs b/extensions/Sisk.Documenting/IApiDocumentationExporter.cs new file mode 100644 index 0000000..4da7bdd --- /dev/null +++ b/extensions/Sisk.Documenting/IApiDocumentationExporter.cs @@ -0,0 +1,14 @@ +namespace Sisk.Documenting; + +/// +/// Defines a contract for exporting API documentation content. +/// +public interface IApiDocumentationExporter { + + /// + /// Exports the specified API documentation content. + /// + /// The API documentation to export. + /// An representing the exported documentation. + public HttpContent ExportDocumentationContent ( ApiDocumentation documentation ); +} \ No newline at end of file diff --git a/extensions/Sisk.Helpers.Swagger/Sisk.Helpers.Swagger.csproj b/extensions/Sisk.Documenting/Sisk.Documenting.csproj similarity index 75% rename from extensions/Sisk.Helpers.Swagger/Sisk.Helpers.Swagger.csproj rename to extensions/Sisk.Documenting/Sisk.Documenting.csproj index 5f3d591..9d8db42 100644 --- a/extensions/Sisk.Helpers.Swagger/Sisk.Helpers.Swagger.csproj +++ b/extensions/Sisk.Documenting/Sisk.Documenting.csproj @@ -6,10 +6,6 @@ enable - - - - diff --git a/extensions/Sisk.Helpers.Swagger/ApiDefinitionAttribute.cs b/extensions/Sisk.Helpers.Swagger/ApiDefinitionAttribute.cs deleted file mode 100644 index 7bb7472..0000000 --- a/extensions/Sisk.Helpers.Swagger/ApiDefinitionAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Sisk.Helpers.Swagger; - -[AttributeUsage ( AttributeTargets.Method, AllowMultiple = false )] -public sealed class ApiDefinitionAttribute : Attribute { - public string? Description { get; set; } - - public ApiDefinitionAttribute () { - } -} diff --git a/extensions/Sisk.Helpers.Swagger/ApiResponseAttribute.cs b/extensions/Sisk.Helpers.Swagger/ApiResponseAttribute.cs deleted file mode 100644 index 257cf54..0000000 --- a/extensions/Sisk.Helpers.Swagger/ApiResponseAttribute.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Net; - -namespace Sisk.Helpers.Swagger; - -[AttributeUsage ( AttributeTargets.Method, AllowMultiple = true )] -public sealed class ApiResponseAttribute : Attribute { - public HttpStatusCode StatusCode { get; set; } - public string? Description { get; set; } - public string? ContentExample { get; set; } - public string? ContentType { get; set; } -} diff --git a/extensions/Sisk.Helpers.Swagger/OpenApiGenerator.cs b/extensions/Sisk.Helpers.Swagger/OpenApiGenerator.cs deleted file mode 100644 index b243639..0000000 --- a/extensions/Sisk.Helpers.Swagger/OpenApiGenerator.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System.Reflection; -using Microsoft.OpenApi.Models; -using Microsoft.OpenApi.Writers; -using Sisk.Core.Routing; - -namespace Sisk.Helpers.Swagger; - -public class OpenApiGenerator { - public static void GenerateOpenApiDocument ( Router router ) { - var routes = router.GetDefinedRoutes (); - var openApiDocument = new OpenApiDocument (); - openApiDocument.Paths = new OpenApiPaths (); - openApiDocument.Info = new OpenApiInfo () { - Version = "1.0.0", - Title = "Teste" - }; - - var routeGrouped = routes.GroupBy ( g => g.Path ); - foreach (var routeGroup in routeGrouped) { - string path = routeGroup.Key; - Dictionary operations = new Dictionary ( routeGroup.Count () ); - - foreach (var route in routeGroup) { - var method = route.Action?.Method; - if (method is null) - continue; - - var apiDefinitionAttr = method.GetCustomAttribute (); - var apiResponseAttrs = method.GetCustomAttributes (); - - var description = apiDefinitionAttr?.Description; - - var operationType = route.Method switch { - RouteMethod.Get => OperationType.Get, - RouteMethod.Post => OperationType.Post, - RouteMethod.Put => OperationType.Put, - RouteMethod.Patch => OperationType.Patch, - RouteMethod.Delete => OperationType.Delete, - RouteMethod.Head => OperationType.Head, - RouteMethod.Options => OperationType.Options, - _ => OperationType.Get // should ignore/throw? need handle this - }; - - OpenApiResponses responses = new OpenApiResponses (); - foreach (var apiResponseAttr in apiResponseAttrs) { - responses.Add ( apiResponseAttr.StatusCode.ToString (), new OpenApiResponse () { - Description = apiResponseAttr?.Description, - Content = new Dictionary () { - } - } ); - } - - operations.Add ( operationType, new OpenApiOperation () { - Description = description, - Responses = new OpenApiResponses () { - [ "200" ] = new OpenApiResponse () { - Description = "200", - - } - } - } ); - } - - openApiDocument.Paths.Add ( path, new OpenApiPathItem () { - Operations = operations - } ); - } - - using var sw = new StringWriter (); - openApiDocument.SerializeAsV3 ( new OpenApiJsonWriter ( sw ) ); - - string json = sw.ToString (); - - ; - - //var document = new OpenApiDocument - //{ - // Info = new OpenApiInfo - // { - // Version = "1.0.0", - // Title = "Swagger Petstore (Simple)", - // }, - // Servers = new List - // { - // new OpenApiServer { Url = "http://petstore.swagger.io/api" } - // }, - // Paths = new OpenApiPaths - // { - // ["/pets"] = new OpenApiPathItem - // { - // Operations = new Dictionary - // { - // [OperationType.Get] = new OpenApiOperation - // { - // Description = "Returns all pets from the system that the user has access to", - // Responses = new OpenApiResponses - // { - // ["200"] = new OpenApiResponse - // { - // Description = "OK" - // } - // } - // } - // } - // } - // } - //}; - } -} diff --git a/extensions/Sisk.Helpers.mitmproxy/MitmproxyProvider.cs b/extensions/Sisk.Helpers.mitmproxy/MitmproxyProvider.cs index 05a60fe..94d5d67 100644 --- a/extensions/Sisk.Helpers.mitmproxy/MitmproxyProvider.cs +++ b/extensions/Sisk.Helpers.mitmproxy/MitmproxyProvider.cs @@ -14,24 +14,24 @@ namespace Sisk.Helpers.Mitmproxy; /// -/// Represents a handler for integrating mitmproxy into an HTTP server. +/// Provides a MITM proxy server handler. /// public sealed class MitmproxyProvider : HttpServerHandler { private ChildProcessStartInfo MitmdumpProcessInfo = null!; private readonly Action? setupAction; /// - /// Gets the instance of the mitmdump process. + /// Gets the mitmdump process. /// public IChildProcess MitmdumpProcess { get; private set; } = null!; /// - /// Gets the port on which the mitmproxy is listening. + /// Gets or sets the proxy port. /// public ushort ProxyPort { get; private set; } /// - /// Gets or sets a value indicating whether the mitmproxy should run in silent mode. + /// Gets or sets a value indicating whether to run the mitmdump process silently. /// public bool Silent { get; set; } diff --git a/extensions/Sisk.JsonRPC/Documentation/DocumentationDescriptor.cs b/extensions/Sisk.JsonRPC/Documentation/DocumentationDescriptor.cs index 5054789..c3a2500 100644 --- a/extensions/Sisk.JsonRPC/Documentation/DocumentationDescriptor.cs +++ b/extensions/Sisk.JsonRPC/Documentation/DocumentationDescriptor.cs @@ -13,7 +13,7 @@ namespace Sisk.JsonRPC.Documentation; internal class DocumentationDescriptor { - internal static JsonRpcDocumentation GetDocumentationDescriptor ( JsonRpcHandler handler ) { + internal static JsonRpcDocumentation GetDocumentationDescriptor ( JsonRpcHandler handler, JsonRpcDocumentationMetadata? metadata ) { List methods = new List (); @@ -36,6 +36,6 @@ internal static JsonRpcDocumentation GetDocumentationDescriptor ( JsonRpcHandler .ToArray () ) ); } - return new JsonRpcDocumentation ( methods.ToArray () ); + return new JsonRpcDocumentation ( methods.ToArray (), metadata ); } } diff --git a/extensions/Sisk.JsonRPC/Documentation/JsonRpcDocumentation.cs b/extensions/Sisk.JsonRPC/Documentation/JsonRpcDocumentation.cs index 683411f..c4140e7 100644 --- a/extensions/Sisk.JsonRPC/Documentation/JsonRpcDocumentation.cs +++ b/extensions/Sisk.JsonRPC/Documentation/JsonRpcDocumentation.cs @@ -15,17 +15,21 @@ namespace Sisk.JsonRPC.Documentation; /// Represents the documentation for JSON-RPC methods. /// public sealed class JsonRpcDocumentation { + /// /// Gets the collection of JSON-RPC methods. /// public JsonRpcDocumentationMethod [] Methods { get; } /// - /// Initializes a new instance of the class with the specified methods. + /// Gets the used for this + /// . /// - /// The array of JSON-RPC methods. - internal JsonRpcDocumentation ( JsonRpcDocumentationMethod [] methods ) { + public JsonRpcDocumentationMetadata? Metadata { get; } + + internal JsonRpcDocumentation ( JsonRpcDocumentationMethod [] methods, JsonRpcDocumentationMetadata? metadata ) { this.Methods = methods; + this.Metadata = metadata; } /// @@ -50,6 +54,32 @@ public string ExportToJson ( JsonOptions options ) { public string ExportToJson () => this.ExportToJson ( JsonOptions.Default ); } +/// +/// Represents the documentation metadata for JSON-RPC documentation. +/// +public sealed class JsonRpcDocumentationMetadata { + /// + /// Gets or sets the name of the application. + /// + public string? ApplicationName { get; set; } + + /// + /// Gets or sets the description of the application. + /// + public string? ApplicationDescription { get; set; } + + /// + /// Gets or sets the path where the JSON-RPC service can receive remote procedures. + /// + public string? ServicePath { get; set; } + + /// + /// Gets or sets an array of that are allowed for the JSON-RPC service at + /// . + /// + public string [] AllowedMethods { get; set; } = [ "POST" ]; +} + /// /// Represents the documentation for a single JSON-RPC method. /// diff --git a/extensions/Sisk.JsonRPC/Documentation/JsonRpcHtmlExport.cs b/extensions/Sisk.JsonRPC/Documentation/JsonRpcHtmlExport.cs index 59fbd6e..3168f75 100644 --- a/extensions/Sisk.JsonRPC/Documentation/JsonRpcHtmlExport.cs +++ b/extensions/Sisk.JsonRPC/Documentation/JsonRpcHtmlExport.cs @@ -22,6 +22,12 @@ public class JsonRpcHtmlExport : IJsonRpcDocumentationExporter { /// public bool ExportSummary { get; set; } = true; + /// + /// Gets or sets an boolean indicating if the documentation metadata should be exported in the + /// HTML. + /// + public bool ExportMetadata { get; set; } = true; + /// /// Gets or sets an optional object to append to the header of the /// exported HTML. @@ -90,6 +96,20 @@ h2 a { opacity: 1; } + hr { + border-top: none; + border-bottom: 3px solid #d1d9e0; + } + + pre { + background-color: #f6f8fa; + color: black; + padding: 1rem; + overflow: auto; + font-size: 85%; + line-height: 1.45; + } + .paramlist { padding-left: 0; list-style-type: none; @@ -165,6 +185,19 @@ string GetMethodIdName ( string name ) { main += this.Header; } + if (this.ExportMetadata && documentation.Metadata is { } meta) { + main += new HtmlElement ( "h1", meta.ApplicationName ?? "Application name" ); + if (meta.ApplicationDescription is not null) { + foreach (string part in meta.ApplicationDescription.Split ( "\n", StringSplitOptions.RemoveEmptyEntries )) { + main += new HtmlElement ( "p", part ); + } + } + main += new HtmlElement ( "p", "Service endpoint:" ); + main += new HtmlElement ( "pre", + string.Join ( "\n", meta.AllowedMethods.Select ( method => $"{method} {meta.ServicePath}" ) ) ); + main += new HtmlElement ( "hr" ).SelfClosed (); + } + if (this.ExportSummary) { main += new HtmlElement ( "h1", "Summary" ); diff --git a/extensions/Sisk.JsonRPC/Documentation/JsonRpcJsonExport.cs b/extensions/Sisk.JsonRPC/Documentation/JsonRpcJsonExport.cs index 47d29d0..1105477 100644 --- a/extensions/Sisk.JsonRPC/Documentation/JsonRpcJsonExport.cs +++ b/extensions/Sisk.JsonRPC/Documentation/JsonRpcJsonExport.cs @@ -26,7 +26,7 @@ public sealed class JsonRpcJsonExport : IJsonRpcDocumentationExporter { /// Creates an new instance with default parameters. /// public JsonRpcJsonExport () { - this.JsonOptions = JsonOptions.Default; + this.JsonOptions = new JsonOptions () { NamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase }; } /// @@ -48,15 +48,15 @@ public JsonValue EncodeDocumentation ( JsonRpcDocumentation documentation ) { foreach (var method in documentation.Methods) { var item = new { - name = method.MethodName, - description = method.Description, - returns = method.ReturnType, - parameters = method.Parameters + Name = method.MethodName, + Description = method.Description, + Returns = method.ReturnType, + Parameters = method.Parameters .Select ( p => new { - name = p.ParameterName, - typeName = p.ParameterType.Name, - description = p.Description, - isOptional = p.IsOptional + Name = p.ParameterName, + TypeName = p.ParameterType.Name, + Description = p.Description, + IsOptional = p.IsOptional } ) .ToArray () }; @@ -64,7 +64,15 @@ public JsonValue EncodeDocumentation ( JsonRpcDocumentation documentation ) { arr.Add ( JsonValue.Serialize ( item, this.JsonOptions ) ); } - return arr.AsJsonValue (); + return this.JsonOptions.Serialize ( new { + Metadata = this.Pipe ( documentation.Metadata, m => new { + m.ApplicationName, + m.ApplicationDescription, + m.ServicePath, + m.AllowedMethods + } ), + Methods = arr + } ); } /// @@ -72,4 +80,6 @@ public byte [] ExportDocumentBytes ( JsonRpcDocumentation documentation ) { string json = this.EncodeDocumentation ( documentation ).ToString (); return Encoding.UTF8.GetBytes ( json ); } + + U Pipe ( T value, Func transform ) => transform ( value ); } diff --git a/extensions/Sisk.JsonRPC/JsonRpcHandler.cs b/extensions/Sisk.JsonRPC/JsonRpcHandler.cs index d52b435..5804089 100644 --- a/extensions/Sisk.JsonRPC/JsonRpcHandler.cs +++ b/extensions/Sisk.JsonRPC/JsonRpcHandler.cs @@ -53,7 +53,13 @@ public JsonRpcHandler () { /// /// Gets the documentation for this JSON-RPC handler. /// - public JsonRpcDocumentation GetDocumentation () { - return DocumentationDescriptor.GetDocumentationDescriptor ( this ); + public JsonRpcDocumentation GetDocumentation () => this.GetDocumentation ( null! ); + + /// + /// Gets the documentation for this JSON-RPC handler. + /// + /// The to generate in the documentation. + public JsonRpcDocumentation GetDocumentation ( JsonRpcDocumentationMetadata metadata ) { + return DocumentationDescriptor.GetDocumentationDescriptor ( this, metadata ); } } diff --git a/extensions/Sisk.JsonRPC/JsonRpcTransportLayer.cs b/extensions/Sisk.JsonRPC/JsonRpcTransportLayer.cs index 44ad2e9..99fc4e9 100644 --- a/extensions/Sisk.JsonRPC/JsonRpcTransportLayer.cs +++ b/extensions/Sisk.JsonRPC/JsonRpcTransportLayer.cs @@ -15,7 +15,6 @@ using Sisk.Core.Http; using Sisk.Core.Http.Streams; using Sisk.Core.Routing; -using Sisk.JsonRPC.Documentation; namespace Sisk.JsonRPC; @@ -44,11 +43,6 @@ internal JsonRpcTransportLayer ( JsonRpcHandler handler ) { /// public RouteAction HttpGet { get => new RouteAction ( this.ImplTransportGetHttp ); } - /// - /// Gets the action to display general help for available web methods. - /// - public RouteAction HttpDescriptor { get => new RouteAction ( this.ImplDescriptor ); } - void ImplWebSocket ( object? sender, WebSocketMessage message ) { JsonRpcRequest? rpcRequest = null; JsonRpcResponse response; @@ -73,17 +67,6 @@ void ImplWebSocket ( object? sender, WebSocketMessage message ) { } ); } - HttpResponse ImplDescriptor ( HttpRequest request ) { - var documentation = DocumentationDescriptor.GetDocumentationDescriptor ( this._handler ); - JsonValue result = new JsonRpcJsonExport ( this._handler._jsonOptions ).EncodeDocumentation ( documentation ); - JsonRpcResponse response = new JsonRpcResponse ( result, null, JsonValue.Null ); - - return new HttpResponse () { - Status = HttpStatusInformation.Ok, - Content = new StringContent ( JsonValue.Serialize ( response, this._handler._jsonOptions ).ToString (), Encoding.UTF8, "application/json" ) - }; - } - HttpResponse ImplTransportGetHttp ( HttpRequest request ) { JsonRpcRequest? rpcRequest = null; JsonRpcResponse response; diff --git a/src/Http/Handlers/DefaultHttpServerHandler.cs b/src/Http/Handlers/DefaultHttpServerHandler.cs index 27113eb..82bc7ae 100644 --- a/src/Http/Handlers/DefaultHttpServerHandler.cs +++ b/src/Http/Handlers/DefaultHttpServerHandler.cs @@ -13,12 +13,12 @@ namespace Sisk.Core.Http.Handlers; internal class DefaultHttpServerHandler : HttpServerHandler { internal Action? _routerSetup; - internal List _serverBootstrapingFunctions = new List (); + internal List _serverBootstrapingFunctions = new List (); protected override void OnServerStarting ( HttpServer server ) { base.OnServerStarting ( server ); foreach (var func in this._serverBootstrapingFunctions) { - func.DynamicInvoke (); + func (); } } diff --git a/src/Routing/Route.cs b/src/Routing/Route.cs index a2afd97..b5ea597 100644 --- a/src/Routing/Route.cs +++ b/src/Routing/Route.cs @@ -163,6 +163,20 @@ internal bool TrySetRouteAction ( MethodInfo method, object? target, [NotNullWhe return true; } + int GetMethodHashCode ( MethodInfo? method ) { + if (method is null) + return 0; + + int carry = HashCode.Combine ( method.Name, method.DeclaringType?.FullName, method.ReturnType, method.IsSpecialName ); + var methodParams = method.GetParameters (); + for (int i = 0; i < methodParams.Length; i++) { + var param = methodParams [ i ]; + carry = HashCode.Combine ( param.Name, param.ParameterType, param.MetadataToken, carry ); + } + + return carry; + } + /// /// Gets or sets the request handlers instances to run before the route's Action. /// @@ -217,7 +231,9 @@ public override string ToString () { /// public override int GetHashCode () { - return HashCode.Combine ( this.path, this.Method, this.UseRegex ); + return HashCode.Combine ( this.path, this.Method, this.UseRegex, + this.GetMethodHashCode ( this._singleParamCallback?.Method ), + this.GetMethodHashCode ( this._parameterlessRouteAction?.Method ) ); } /// @@ -237,14 +253,14 @@ public bool Equals ( Route? other ) { public static bool operator == ( Route? left, Route? right ) { if (left is null && right is null) return true; - return left?.Equals ( right ) == true; + if (left is null || right is null) + return false; + return left.Equals ( right ); } /// public static bool operator != ( Route? left, Route? right ) { - if (left is null && right is null) - return false; - return !left?.Equals ( right ) == true; + return !(left == right); } #region Helper constructors diff --git a/src/Routing/Router__CoreSetters.cs b/src/Routing/Router__CoreSetters.cs index 973d3ef..0b23d0c 100644 --- a/src/Routing/Router__CoreSetters.cs +++ b/src/Routing/Router__CoreSetters.cs @@ -333,7 +333,7 @@ private void SetObjectInternal ( MethodInfo [] methods, Type callerType, object? continue; } - RouteAttribute? routeAttribute = null; + List routeAttributes = new List (); object [] methodAttributes = method.GetCustomAttributes ( true ); List methodAttrReqHandlers = new List ( methodAttributes.Length ); @@ -348,11 +348,11 @@ private void SetObjectInternal ( MethodInfo [] methods, Type callerType, object? methodAttrReqHandlers.Add ( reqHandlerAttr.Activate () ); } else if (attrInstance is RouteAttribute routeAttributeItem) { - routeAttribute = routeAttributeItem; + routeAttributes.Add ( routeAttributeItem ); } } - if (routeAttribute is not null) { + foreach (var routeAttribute in routeAttributes) { try { string path = routeAttribute.Path;