Skip to content

Commit 5c50939

Browse files
committed
openapi support added
1 parent 115b84a commit 5c50939

25 files changed

+936
-49
lines changed

Readme.md

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,117 @@
1-
# Pandatech.***
1+
# Pandatech.SharedKernel
22

3-
## Introduction
3+
Welcome to the `Pandatech.SharedKernel` NuGet package, a centralized library designed to streamline development across all PandaTech projects. This package consolidates shared configurations, utilities, and extensions into a single, reusable resource.
44

5-
## Features
5+
Though this package is primarily intended for internal use, it is publicly available for anyone who may find it useful. We recommend forking or copying the classes in this repository and creating your own package to suit your needs.
66

7-
## Installation
7+
By leveraging this shared kernel, we aim to:
88

9-
## Usage
9+
- Reduce the amount of boilerplate code required to start a new project.
10+
- Ensure consistency across all PandaTech projects.
11+
- Simplify the process of updating shared configurations and utilities.
1012

11-
## License
13+
## Scope
1214

13-
Pandatech.*** is licensed under the MIT License.
15+
This package currently supports:
16+
17+
- **OpenAPI Configuration** with SwaggerUI and Scalar.
18+
19+
20+
## OpenAPI
21+
22+
`Microsoft.AspNetCore.OpenApi` is the new standard for creating OpenAPI JSON files. We have adopted this library instead of Swashbuckle for generating OpenAPI definitions. While using this new library, we have integrated `SwaggerUI` and `Scalar` to provide user-friendly interfaces in addition to the JSON files.
23+
24+
### Key Features
25+
26+
- **Multiple API Documents:** Easily define and organize multiple API documentation groups.
27+
- **Enum String Values:** Enum string values are automatically displayed in the documentation, simplifying integration for external partners.
28+
- **Customizable SwaggerUI:** Add custom styles and JavaScript to tailor the UI.
29+
- **Security Schemes:** Configure security headers directly in your OpenAPI settings.
30+
31+
32+
### Adding OpenAPI to Your Project
33+
34+
To enable OpenAPI in your project, add the following code:
35+
36+
```csharp
37+
var builder = WebApplication.CreateBuilder(args);
38+
builder.AddOpenApi();
39+
var app = builder.Build();
40+
app.UseOpenApi();
41+
app.Run();
42+
```
43+
44+
You can also customize the `AddOpenApi` method with options:
45+
46+
```csharp
47+
builder.AddOpenApi(options =>
48+
{
49+
options.AddSchemaTransformer<CustomSchemaTransformer>();
50+
});
51+
```
52+
53+
### Configuration
54+
55+
Add the following configuration to your `appsettings.json` file:
56+
57+
```json
58+
{
59+
"OpenApi": {
60+
"DisabledEnvironments": [
61+
"Production"
62+
],
63+
"SecuritySchemes": [
64+
{
65+
"HeaderName": "Authorization",
66+
"Description": "Access token for the API."
67+
}
68+
],
69+
"Documents": [
70+
{
71+
"Title": "Admin Panel API",
72+
"Description": "API for administrative functions.",
73+
"GroupName": "admin-v1",
74+
"Version": "v1",
75+
"ForExternalUse": false
76+
},
77+
{
78+
"Title": "Integration",
79+
"Description": "Integration API Endpoints",
80+
"GroupName": "integration-v1",
81+
"Version": "v1",
82+
"ForExternalUse": true
83+
}
84+
],
85+
"Contact": {
86+
"Name": "Pandatech",
87+
"Url": "https://pandatech.it",
88+
"Email": "[email protected]"
89+
}
90+
},
91+
"SwaggerUi": {
92+
"InjectedCssPaths": [
93+
"/assets/css/panda-style.css"
94+
],
95+
"InjectedJsPaths": [
96+
"/assets/js/docs.js"
97+
]
98+
},
99+
"ScalarUi": {
100+
"FaviconPath": "/assets/images/favicon.svg"
101+
}
102+
}
103+
```
104+
105+
### Notes
106+
107+
- **For External Use:** If you set `ForExternalUse: true` for a document, it will be available both within the regular SwaggerUI and a separate SwaggerUI instance. This allows you to provide a dedicated URL to external partners while keeping internal documents private.
108+
- **Scalar UI Limitations:** Scalar currently does not support multiple documents within a single URL. Consequently, all documents in Scalar will be separated into individual URLs. Support for multiple documents is expected in future Scalar updates.
109+
110+
### Example URLs
111+
112+
Based on the above configuration, the UI will be accessible at the following URLs:
113+
114+
- **Swagger (all documents):** [http://localhost/swagger](http://localhost/swagger)
115+
- **Swagger (external document only):** [http://localhost/doc/integration-v1](http://localhost/doc/integration-v1)
116+
- **Scalar (admin document):** [http://localhost/scalar/admin-v1](http://localhost/scalar/admin-v1)
117+
- **Scalar (integration document):** [http://localhost/scalar/integration-v1](http://localhost/scalar/integration-v1)

PandaNuGet.sln renamed to SharedKernel.sln

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PandaNuGet", "src\PandaNuGet\PandaNuGet.csproj", "{25001943-A870-4E17-A9B9-0D190CEC819B}"
3+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedKernel", "src\SharedKernel\SharedKernel.csproj", "{25001943-A870-4E17-A9B9-0D190CEC819B}"
44
EndProject
55
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PandaNuGet.Tests", "test\PandaNuGet.Tests\PandaNuGet.Tests.csproj", "{0305E58F-1C47-454C-B10B-A223F2561A85}"
66
EndProject

src/PandaNuGet/PandaNuGet.csproj

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace PandaNuGet;
1+
namespace SharedKernel;
22

33
public class Class1
44
{
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Microsoft.AspNetCore.OpenApi;
2+
using Microsoft.OpenApi.Models;
3+
4+
namespace SharedKernel.OpenApi;
5+
6+
internal class EnumSchemaTransformer : IOpenApiSchemaTransformer
7+
{
8+
public Task TransformAsync(OpenApiSchema schema,
9+
OpenApiSchemaTransformerContext context,
10+
CancellationToken cancellationToken)
11+
{
12+
var type = context.JsonTypeInfo.Type;
13+
14+
if (!type.IsEnum)
15+
{
16+
return Task.CompletedTask;
17+
}
18+
19+
var enumDescriptions = Enum.GetValues(type)
20+
.Cast<object>()
21+
.Select(value => $"{value} = {(int)value}")
22+
.ToList();
23+
24+
schema.Description = string.Join(", ", enumDescriptions);
25+
26+
return Task.CompletedTask;
27+
}
28+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using Microsoft.AspNetCore.Builder;
3+
using Microsoft.AspNetCore.OpenApi;
4+
using Microsoft.Extensions.Configuration;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.Hosting;
7+
using RegexBox;
8+
using Scalar.AspNetCore;
9+
using SharedKernel.OpenApi.Options;
10+
11+
namespace SharedKernel.OpenApi;
12+
13+
public static class OpenApiExtensions
14+
{
15+
public static WebApplicationBuilder AddOpenApi(this WebApplicationBuilder builder, Action<OpenApiOptions>? configureOptions = null)
16+
{
17+
var openApiConfiguration = builder.Configuration
18+
.GetSection("OpenApi")
19+
.Get<OpenApiConfig>();
20+
21+
if (builder.Environment.IsOpenApiConfigValidAndDisabled(openApiConfiguration))
22+
{
23+
return builder;
24+
}
25+
26+
27+
foreach (var document in openApiConfiguration.Documents)
28+
{
29+
builder.Services.AddOpenApi(document.GroupName,
30+
options =>
31+
{
32+
options.AddDocument(document, openApiConfiguration);
33+
options.AddSchemaTransformer<EnumSchemaTransformer>();
34+
options.UseApiSecuritySchemes(openApiConfiguration);
35+
configureOptions?.Invoke(options);
36+
});
37+
}
38+
39+
return builder;
40+
}
41+
42+
public static WebApplication UseOpenApi(this WebApplication app)
43+
{
44+
var openApiConfiguration = app.Configuration
45+
.GetSection("OpenApi")
46+
.Get<OpenApiConfig>();
47+
48+
if (app.Environment.IsOpenApiConfigValidAndDisabled(openApiConfiguration))
49+
{
50+
return app;
51+
}
52+
53+
app.MapStaticAssets();
54+
app.MapOpenApi();
55+
app.MapSwaggerUi(openApiConfiguration);
56+
app.MapScalarApiReference(options =>
57+
{
58+
options.Theme = ScalarTheme.Kepler;
59+
options.Favicon = "/assets/images/favicon.svg";
60+
});
61+
return app;
62+
}
63+
64+
private static bool IsOpenApiConfigValidAndDisabled(this IHostEnvironment environment,
65+
[NotNull] OpenApiConfig? openApiConfiguration)
66+
{
67+
if (openApiConfiguration is null || openApiConfiguration.Documents.Count == 0)
68+
{
69+
throw new InvalidOperationException("OpenApi configuration is missing or contains no documents.");
70+
}
71+
72+
// Validate Contact
73+
if (openApiConfiguration.Contact is null)
74+
{
75+
throw new InvalidOperationException("Contact configuration is required in OpenApi.");
76+
}
77+
78+
if (string.IsNullOrWhiteSpace(openApiConfiguration.Contact.Name))
79+
{
80+
throw new InvalidOperationException("Contact Name is required in OpenApi configuration.");
81+
}
82+
83+
if (!PandaValidator.IsUri(openApiConfiguration.Contact.Url))
84+
{
85+
throw new InvalidOperationException("Contact URL must be a valid URL in OpenApi configuration.");
86+
}
87+
88+
if (!PandaValidator.IsEmail(openApiConfiguration.Contact.Email))
89+
{
90+
throw new InvalidOperationException("Contact Email is required in OpenApi configuration.");
91+
}
92+
93+
// Validate OpenApi documents
94+
foreach (var document in openApiConfiguration.Documents)
95+
{
96+
if (string.IsNullOrWhiteSpace(document.Title))
97+
{
98+
throw new InvalidOperationException("Document Title is required in OpenApi configuration.");
99+
}
100+
101+
if (string.IsNullOrWhiteSpace(document.Description))
102+
{
103+
throw new InvalidOperationException(
104+
$"Document Description is required for document '{document.Title}' in OpenApi configuration.");
105+
}
106+
107+
if (string.IsNullOrWhiteSpace(document.GroupName))
108+
{
109+
throw new InvalidOperationException(
110+
$"GroupName is required for document '{document.Title}' in OpenApi configuration.");
111+
}
112+
113+
if (string.IsNullOrWhiteSpace(document.Version))
114+
{
115+
throw new InvalidOperationException(
116+
$"Version is required for document '{document.Title}' in OpenApi configuration.");
117+
}
118+
119+
document.GroupName = document.GroupName.ToLowerInvariant();
120+
}
121+
122+
// Validate SecuritySchemes
123+
foreach (var schema in openApiConfiguration.SecuritySchemes)
124+
{
125+
if (string.IsNullOrWhiteSpace(schema.HeaderName))
126+
{
127+
throw new InvalidOperationException("SecuritySchema HeaderName is required in OpenApi configuration..");
128+
}
129+
130+
if (string.IsNullOrWhiteSpace(schema.Description))
131+
{
132+
throw new InvalidOperationException(
133+
$"Description is required for SecuritySchema with HeaderName '{schema.HeaderName}' in OpenApi configuration..");
134+
}
135+
}
136+
137+
// Check if the environment is disabled
138+
return openApiConfiguration.DisabledEnvironments.Contains(environment.EnvironmentName);
139+
}
140+
}

0 commit comments

Comments
 (0)