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;