Initial version

This commit is contained in:
2026-06-20 10:24:36 +02:00
parent f85b83d90f
commit 7e1173bf2c
40 changed files with 5438 additions and 63 deletions
+4
View File
@@ -0,0 +1,4 @@
namespace EonaCat.DoxaApi.Exporter
{
public static class DoxaApiExporter { }
}
+272
View File
@@ -0,0 +1,272 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using EonaCat.DoxaApi.Models;
namespace EonaCat.DoxaApi.Interop
{
public static class OpenApiExporter
{
public static JsonObject Export(ApiDocument doc)
{
var root = new JsonObject
{
["openapi"] = "3.0.3",
["info"] = BuildInfo(doc.Info),
};
if (doc.Servers.Count > 0)
{
var servers = new JsonArray();
foreach (var s in doc.Servers)
{
servers.Add(new JsonObject { ["url"] = s });
}
root["servers"] = servers;
}
var paths = new JsonObject();
foreach (var group in doc.Groups)
{
foreach (var endpoint in group.Endpoints)
{
var openApiPath = ToOpenApiPath(endpoint.Path);
if (!paths.ContainsKey(openApiPath))
{
paths[openApiPath] = new JsonObject();
}
var pathItem = (JsonObject)paths[openApiPath]!;
pathItem[endpoint.Method.ToLowerInvariant()] = BuildOperation(endpoint, group.Name);
}
}
root["paths"] = paths;
if (doc.Schemas.Count > 0)
{
var schemas = new JsonObject();
foreach (var (name, schema) in doc.Schemas)
{
schemas[name] = SchemaToOpenApi(schema);
}
root["components"] = new JsonObject { ["schemas"] = schemas };
}
return root;
}
private static JsonObject BuildInfo(ApiInfo info)
{
var obj = new JsonObject
{
["title"] = info.Title,
["version"] = info.Version
};
if (info.Description is not null)
{
obj["description"] = info.Description;
}
return obj;
}
private static JsonObject BuildOperation(ApiEndpoint endpoint, string groupName)
{
var op = new JsonObject();
var tags = new JsonArray();
foreach (var t in (endpoint.Tags.Count > 0 ? endpoint.Tags : new List<string> { groupName }))
{
tags.Add(t);
}
op["tags"] = tags;
op["operationId"] = endpoint.OperationId;
if (endpoint.Summary is not null)
{
op["summary"] = endpoint.Summary;
}
if (endpoint.Description is not null)
{
op["description"] = endpoint.Description;
}
if (endpoint.Deprecated)
{
op["deprecated"] = true;
}
if (endpoint.Parameters.Count > 0)
{
var parameters = new JsonArray();
foreach (var p in endpoint.Parameters)
{
var param = new JsonObject
{
["name"] = p.Name,
["in"] = p.In,
["required"] = p.Required,
["schema"] = SchemaToOpenApi(p.Schema)
};
if (p.Description is not null)
{
param["description"] = p.Description;
}
parameters.Add(param);
}
op["parameters"] = parameters;
}
if (endpoint.RequestBody is not null)
{
var rb = endpoint.RequestBody;
var content = new JsonObject
{
[rb.ContentType] = new JsonObject { ["schema"] = SchemaToOpenApi(rb.Schema) }
};
if (rb.Example is not null)
{
((JsonObject)content[rb.ContentType]!)["example"] =
JsonNode.Parse(rb.Example) ?? JsonValue.Create(rb.Example)!;
}
op["requestBody"] = new JsonObject
{
["required"] = rb.Required,
["content"] = content
};
}
var responses = new JsonObject();
foreach (var r in endpoint.Responses)
{
var resp = new JsonObject();
resp["description"] = r.Description ?? HttpStatusDescription(r.StatusCode);
if (r.Schema is not null && r.Schema.Type != "void")
{
resp["content"] = new JsonObject
{
["application/json"] = new JsonObject
{
["schema"] = SchemaToOpenApi(r.Schema)
}
};
}
responses[r.StatusCode] = resp;
}
op["responses"] = responses;
return op;
}
private static JsonObject SchemaToOpenApi(SchemaModel schema)
{
if (schema.RefName is not null)
{
return new JsonObject { ["$ref"] = $"#/components/schemas/{schema.RefName}" };
}
var obj = new JsonObject();
switch (schema.Type)
{
case "void":
return new JsonObject { ["type"] = "object" };
case "enum":
obj["type"] = "string";
if (schema.EnumValues?.Count > 0)
{
var enums = new JsonArray();
foreach (var v in schema.EnumValues)
{
enums.Add(v);
}
obj["enum"] = enums;
}
break;
case "array":
obj["type"] = "array";
if (schema.Items is not null)
{
obj["items"] = SchemaToOpenApi(schema.Items);
}
break;
case "object":
obj["type"] = "object";
if (schema.Properties?.Count > 0)
{
var props = new JsonObject();
foreach (var (name, propSchema) in schema.Properties)
{
props[name] = SchemaToOpenApi(propSchema);
}
obj["properties"] = props;
}
if (schema.Required?.Count > 0)
{
var req = new JsonArray();
foreach (var r in schema.Required)
{
req.Add(r);
}
obj["required"] = req;
}
if (schema.Items is not null && schema.Properties is null)
{
obj["additionalProperties"] = SchemaToOpenApi(schema.Items);
}
break;
default:
obj["type"] = schema.Type;
if (schema.Format is not null)
{
obj["format"] = schema.Format;
}
break;
}
if (schema.Nullable)
{
obj["nullable"] = true;
}
return obj;
}
private static string ToOpenApiPath(string path)
=> path.StartsWith('/') ? path : "/" + path;
private static string HttpStatusDescription(string code) => code switch
{
"200" => "OK",
"201" => "Created",
"204" => "No Content",
"400" => "Bad Request",
"401" => "Unauthorized",
"403" => "Forbidden",
"404" => "Not Found",
"409" => "Conflict",
"422" => "Unprocessable Entity",
"500" => "Internal Server Error",
_ => "Response"
};
}
}
+304
View File
@@ -0,0 +1,304 @@
using System.Text.Json.Nodes;
using EonaCat.DoxaApi.Models;
namespace EonaCat.DoxaApi.Interop
{
public static class SwaggerExporter
{
public static JsonObject Export(ApiDocument doc)
{
var root = new JsonObject
{
["swagger"] = "2.0",
["info"] = BuildInfo(doc.Info),
};
if (doc.Servers.Count > 0 && Uri.TryCreate(doc.Servers[0], UriKind.Absolute, out var uri))
{
root["host"] = uri.Host + (uri.IsDefaultPort ? "" : $":{uri.Port}");
root["basePath"] = string.IsNullOrEmpty(uri.AbsolutePath) ? "/" : uri.AbsolutePath;
var schemes = new JsonArray();
schemes.Add(uri.Scheme);
root["schemes"] = schemes;
}
else
{
root["basePath"] = "/";
}
root["consumes"] = new JsonArray { "application/json" };
root["produces"] = new JsonArray { "application/json" };
var paths = new JsonObject();
foreach (var group in doc.Groups)
{
foreach (var endpoint in group.Endpoints)
{
var swaggerPath = ToSwaggerPath(endpoint.Path);
if (!paths.ContainsKey(swaggerPath))
{
paths[swaggerPath] = new JsonObject();
}
var pathItem = (JsonObject)paths[swaggerPath]!;
pathItem[endpoint.Method.ToLowerInvariant()] = BuildOperation(endpoint, group.Name);
}
}
root["paths"] = paths;
if (doc.Schemas.Count > 0)
{
var definitions = new JsonObject();
foreach (var (name, schema) in doc.Schemas)
{
definitions[name] = SchemaToSwagger(schema);
}
root["definitions"] = definitions;
}
return root;
}
private static JsonObject BuildInfo(ApiInfo info)
{
var obj = new JsonObject
{
["title"] = info.Title,
["version"] = info.Version
};
if (info.Description is not null)
{
obj["description"] = info.Description;
}
return obj;
}
private static JsonObject BuildOperation(ApiEndpoint endpoint, string groupName)
{
var op = new JsonObject();
var tags = new JsonArray();
foreach (var t in (endpoint.Tags.Count > 0 ? endpoint.Tags : new List<string> { groupName }))
{
tags.Add(t);
}
op["tags"] = tags;
op["operationId"] = endpoint.OperationId;
if (endpoint.Summary is not null)
{
op["summary"] = endpoint.Summary;
}
if (endpoint.Description is not null)
{
op["description"] = endpoint.Description;
}
if (endpoint.Deprecated)
{
op["deprecated"] = true;
}
var parameters = new JsonArray();
foreach (var p in endpoint.Parameters)
{
var param = new JsonObject
{
["name"] = p.Name,
["in"] = p.In,
["required"] = p.Required,
};
if (p.Description is not null)
{
param["description"] = p.Description;
}
InlineSchemaIntoParam(param, p.Schema);
parameters.Add(param);
}
if (endpoint.RequestBody is not null)
{
var rb = endpoint.RequestBody;
var bodyParam = new JsonObject
{
["name"] = "body",
["in"] = "body",
["required"] = rb.Required,
["schema"] = SchemaToSwagger(rb.Schema)
};
parameters.Add(bodyParam);
}
if (parameters.Count > 0)
{
op["parameters"] = parameters;
}
var responses = new JsonObject();
foreach (var r in endpoint.Responses)
{
var resp = new JsonObject
{
["description"] = r.Description ?? HttpStatusDescription(r.StatusCode)
};
if (r.Schema is not null && r.Schema.Type != "void")
{
resp["schema"] = SchemaToSwagger(r.Schema);
}
responses[r.StatusCode] = resp;
}
op["responses"] = responses;
return op;
}
private static void InlineSchemaIntoParam(JsonObject param, SchemaModel schema)
{
if (schema.RefName is not null)
{
param["type"] = "string";
return;
}
switch (schema.Type)
{
case "array":
param["type"] = "array";
if (schema.Items is not null)
{
var items = new JsonObject();
InlineSchemaIntoParam(items, schema.Items);
param["items"] = items;
}
break;
case "enum":
param["type"] = "string";
if (schema.EnumValues?.Count > 0)
{
var enums = new JsonArray();
foreach (var v in schema.EnumValues)
{
enums.Add(v);
}
param["enum"] = enums;
}
break;
default:
param["type"] = schema.Type == "void" ? "string" : schema.Type;
if (schema.Format is not null)
{
param["format"] = schema.Format;
}
break;
}
}
private static JsonObject SchemaToSwagger(SchemaModel schema)
{
if (schema.RefName is not null)
{
return new JsonObject { ["$ref"] = $"#/definitions/{schema.RefName}" };
}
var obj = new JsonObject();
switch (schema.Type)
{
case "void":
return new JsonObject { ["type"] = "object" };
case "enum":
obj["type"] = "string";
if (schema.EnumValues?.Count > 0)
{
var enums = new JsonArray();
foreach (var v in schema.EnumValues)
{
enums.Add(v);
}
obj["enum"] = enums;
}
break;
case "array":
obj["type"] = "array";
if (schema.Items is not null)
{
obj["items"] = SchemaToSwagger(schema.Items);
}
break;
case "object":
obj["type"] = "object";
if (schema.Properties?.Count > 0)
{
var props = new JsonObject();
foreach (var (name, propSchema) in schema.Properties)
{
props[name] = SchemaToSwagger(propSchema);
}
obj["properties"] = props;
}
if (schema.Required?.Count > 0)
{
var req = new JsonArray();
foreach (var r in schema.Required)
{
req.Add(r);
}
obj["required"] = req;
}
if (schema.Items is not null && schema.Properties is null)
{
obj["additionalProperties"] = SchemaToSwagger(schema.Items);
}
break;
default:
obj["type"] = schema.Type;
if (schema.Format is not null)
{
obj["format"] = schema.Format;
}
break;
}
return obj;
}
private static string ToSwaggerPath(string path)
=> path.StartsWith('/') ? path : "/" + path;
private static string HttpStatusDescription(string code) => code switch
{
"200" => "OK",
"201" => "Created",
"204" => "No Content",
"400" => "Bad Request",
"401" => "Unauthorized",
"403" => "Forbidden",
"404" => "Not Found",
"500" => "Internal Server Error",
_ => "Response"
};
}
}