Files
EonaCat.DoxaApi/DoxaApi/Importer/OpenApiImporter.cs
T
2026-06-21 20:10:02 +02:00

767 lines
26 KiB
C#

using System.Text.Json;
using System.Text.Json.Nodes;
using EonaCat.DoxaApi.Models;
namespace EonaCat.DoxaApi.Interop
{
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/license for full license details.
public static class OpenApiImporter
{
public static ApiDocument Import(string json)
{
var node = JsonNode.Parse(json) ?? throw new ArgumentException("Input is not valid JSON.");
return Import(node);
}
public static async Task<ApiDocument> ImportAsync(Stream stream)
{
var node = await JsonNode.ParseAsync(stream)
?? throw new ArgumentException("Stream is not valid JSON.");
return Import(node);
}
public static ApiDocument Import(JsonNode root)
{
if (root["openapi"] is not null)
{
return ImportOpenApi3(root);
}
if (root["swagger"] is not null)
{
return ImportSwagger2(root);
}
if (root["groups"] is not null &&
root["info"] is not null)
{
return JsonSerializer.Deserialize<ApiDocument>(root)!
?? throw new InvalidOperationException();
}
throw new NotSupportedException(
"Supported formats: DoxaApi, OpenAPI 3.x, Swagger 2.0");
}
private static ApiDocument ImportOpenApi3(JsonNode root)
{
var doc = new ApiDocument();
if (root["info"] is JsonObject info)
{
doc.Info.Title = info["title"]?.GetValue<string>() ?? "API";
doc.Info.Version = info["version"]?.GetValue<string>() ?? "v1";
doc.Info.Description = info["description"]?.GetValue<string>();
}
if (root["servers"] is JsonArray servers)
{
foreach (var s in servers)
{
if (s?["url"]?.GetValue<string>() is string url)
{
doc.Servers.Add(url);
}
}
}
if (root["components"]?["schemas"] is JsonObject compSchemas)
{
foreach (var (name, schemaNode) in compSchemas)
{
if (schemaNode is not null)
{
doc.Schemas[name] = ParseSchema3(schemaNode);
}
}
}
if (root["components"]?["securitySchemes"] is JsonObject compSecuritySchemes)
{
foreach (var (id, schemeNode) in compSecuritySchemes)
{
if (schemeNode is JsonObject schemeObj)
{
doc.SecuritySchemes[id] = ParseSecurityScheme3(id, schemeObj);
}
}
}
var groups = new Dictionary<string, ApiGroup>(StringComparer.OrdinalIgnoreCase);
if (root["paths"] is JsonObject paths)
{
foreach (var (rawPath, pathNode) in paths)
{
if (pathNode is not JsonObject pathItem)
{
continue;
}
foreach (var (methodStr, opNode) in pathItem)
{
if (opNode is not JsonObject op)
{
continue;
}
if (!IsHttpMethod(methodStr))
{
continue;
}
var endpoint = ParseOperation3(op, rawPath, methodStr.ToUpperInvariant());
var groupName = endpoint.Tags.FirstOrDefault() ?? "Default";
if (!groups.TryGetValue(groupName, out var group))
{
group = new ApiGroup { Name = groupName };
groups[groupName] = group;
}
group.Endpoints.Add(endpoint);
}
}
}
doc.Groups = groups.Values
.OrderBy(g => g.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
return doc;
}
private static ApiEndpoint ParseOperation3(JsonObject op, string path, string method)
{
var endpoint = new ApiEndpoint
{
OperationId = op["operationId"]?.GetValue<string>() ?? $"{method}_{SanitizePath(path)}",
Summary = op["summary"]?.GetValue<string>(),
Description = op["description"]?.GetValue<string>(),
Method = method,
Path = path,
Deprecated = op["deprecated"]?.GetValue<bool>() ?? false,
Tags = ParseStringArray(op["tags"]),
Security = ParseSecurityArray(op["security"])
};
if (op["parameters"] is JsonArray opParameters)
{
foreach (var p in opParameters)
{
if (p is not JsonObject param)
{
continue;
}
var schema = param["schema"] is JsonNode schemaNode
? ParseSchema3(schemaNode)
: new SchemaModel { Type = "string" };
endpoint.Parameters.Add(new ApiParameter
{
Name = param["name"]?.GetValue<string>() ?? "",
In = param["in"]?.GetValue<string>() ?? "query",
Required = param["required"]?.GetValue<bool>() ?? false,
Description = param["description"]?.GetValue<string>(),
Schema = schema
});
}
}
if (op["requestBody"] is JsonObject rb)
{
var required = rb["required"]?.GetValue<bool>() ?? false;
var contentNode = rb["content"] as JsonObject;
var (contentType, mediaObj) = PickMediaType(contentNode);
SchemaModel schema;
string? example = null;
if (mediaObj is JsonObject mo)
{
schema = mo["schema"] is JsonNode s ? ParseSchema3(s) : new SchemaModel { Type = "object" };
example = mo["example"]?.ToJsonString();
}
else
{
schema = new SchemaModel { Type = "object" };
}
endpoint.RequestBody = new RequestBodyModel
{
Required = required,
ContentType = contentType,
Schema = schema,
Example = example
};
}
if (op["responses"] is JsonObject responses)
{
foreach (var (statusCode, respNode) in responses)
{
if (respNode is not JsonObject resp)
{
continue;
}
SchemaModel? schema = null;
if (resp["content"] is JsonObject content)
{
var (_, mediaObj) = PickMediaType(content);
if (mediaObj?["schema"] is JsonNode s)
{
schema = ParseSchema3(s);
}
}
endpoint.Responses.Add(new ResponseModel
{
StatusCode = statusCode,
Description = resp["description"]?.GetValue<string>(),
Schema = schema
});
}
}
return endpoint;
}
private static SchemaModel ParseSchema3(JsonNode node)
{
if (node is not JsonObject obj)
{
return new SchemaModel { Type = "object" };
}
if (obj["$ref"]?.GetValue<string>() is string refVal)
{
var refName = refVal.Split('/').Last();
return new SchemaModel { Type = "object", RefName = refName };
}
bool nullable = obj["nullable"]?.GetValue<bool>() ?? false;
if (obj["enum"] is JsonArray enumArray)
{
return new SchemaModel
{
Type = "enum",
EnumValues = enumArray.Select(e => e?.GetValue<string>() ?? "").ToList(),
Nullable = nullable
};
}
var type = obj["type"]?.GetValue<string>() ?? "object";
if (type == "array")
{
return new SchemaModel
{
Type = "array",
Items = obj["items"] is JsonNode items ? ParseSchema3(items) : null,
Nullable = nullable
};
}
if (type == "object")
{
Dictionary<string, SchemaModel>? props = null;
if (obj["properties"] is JsonObject propsNode)
{
props = new Dictionary<string, SchemaModel>();
foreach (var (name, propNode) in propsNode)
{
if (propNode is not null)
{
props[name] = ParseSchema3(propNode);
}
}
}
SchemaModel? dictItems = null;
if (obj["additionalProperties"] is JsonNode addProps)
{
dictItems = ParseSchema3(addProps);
}
return new SchemaModel
{
Type = "object",
Properties = props,
Required = ParseStringList(obj["required"]),
Items = dictItems,
Nullable = nullable
};
}
return new SchemaModel
{
Type = type,
Format = obj["format"]?.GetValue<string>(),
Nullable = nullable
};
}
private static ApiDocument ImportSwagger2(JsonNode root)
{
var doc = new ApiDocument();
if (root["info"] is JsonObject info)
{
doc.Info.Title = info["title"]?.GetValue<string>() ?? "API";
doc.Info.Version = info["version"]?.GetValue<string>() ?? "v1";
doc.Info.Description = info["description"]?.GetValue<string>();
}
var host = root["host"]?.GetValue<string>();
var basePath = root["basePath"]?.GetValue<string>() ?? "/";
var scheme = root["schemes"] is JsonArray schemes && schemes.Count > 0
? schemes[0]?.GetValue<string>() ?? "https"
: "https";
if (host is not null)
{
doc.Servers.Add($"{scheme}://{host}{basePath.TrimEnd('/')}");
}
if (root["definitions"] is JsonObject defs)
{
foreach (var (name, defNode) in defs)
{
if (defNode is not null)
{
doc.Schemas[name] = ParseSchema2(defNode);
}
}
}
if (root["securityDefinitions"] is JsonObject secDefs)
{
foreach (var (id, defNode) in secDefs)
{
if (defNode is JsonObject defObj)
{
doc.SecuritySchemes[id] = ParseSecurityScheme2(id, defObj);
}
}
}
var groups = new Dictionary<string, ApiGroup>(StringComparer.OrdinalIgnoreCase);
if (root["paths"] is JsonObject paths)
{
foreach (var (rawPath, pathNode) in paths)
{
if (pathNode is not JsonObject pathItem)
{
continue;
}
foreach (var (methodStr, opNode) in pathItem)
{
if (opNode is not JsonObject op)
{
continue;
}
if (!IsHttpMethod(methodStr))
{
continue;
}
var endpoint = ParseOperation2(op, rawPath, methodStr.ToUpperInvariant());
var groupName = endpoint.Tags.FirstOrDefault() ?? "Default";
if (!groups.TryGetValue(groupName, out var group))
{
group = new ApiGroup { Name = groupName };
groups[groupName] = group;
}
group.Endpoints.Add(endpoint);
}
}
}
doc.Groups = groups.Values
.OrderBy(g => g.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
return doc;
}
private static ApiEndpoint ParseOperation2(JsonObject op, string path, string method)
{
var endpoint = new ApiEndpoint
{
OperationId = op["operationId"]?.GetValue<string>() ?? $"{method}_{SanitizePath(path)}",
Summary = op["summary"]?.GetValue<string>(),
Description = op["description"]?.GetValue<string>(),
Method = method,
Path = path,
Deprecated = op["deprecated"]?.GetValue<bool>() ?? false,
Tags = ParseStringArray(op["tags"]),
Security = ParseSecurityArray(op["security"])
};
if (op["parameters"] is JsonArray parameters)
{
foreach (var p in parameters)
{
if (p is not JsonObject param)
{
continue;
}
var inLoc = param["in"]?.GetValue<string>() ?? "query";
if (inLoc == "body")
{
endpoint.RequestBody = new RequestBodyModel
{
Required = param["required"]?.GetValue<bool>() ?? false,
ContentType = "application/json",
Schema = param["schema"] is JsonNode s ? ParseSchema2(s) : new SchemaModel { Type = "object" }
};
continue;
}
endpoint.Parameters.Add(new ApiParameter
{
Name = param["name"]?.GetValue<string>() ?? "",
In = inLoc,
Required = param["required"]?.GetValue<bool>() ?? false,
Description = param["description"]?.GetValue<string>(),
Schema = ParseInlineSchema2(param)
});
}
}
if (op["responses"] is JsonObject responses)
{
foreach (var (statusCode, respNode) in responses)
{
if (respNode is not JsonObject resp)
{
continue;
}
SchemaModel? schema = null;
if (resp["schema"] is JsonNode s)
{
schema = ParseSchema2(s);
}
endpoint.Responses.Add(new ResponseModel
{
StatusCode = statusCode,
Description = resp["description"]?.GetValue<string>(),
Schema = schema
});
}
}
return endpoint;
}
private static SchemaModel ParseInlineSchema2(JsonObject param)
{
if (param["$ref"]?.GetValue<string>() is string refVal)
{
return new SchemaModel { Type = "object", RefName = refVal.Split('/').Last() };
}
if (param["enum"] is JsonArray enumArray)
{
return new SchemaModel
{
Type = "enum",
EnumValues = enumArray.Select(e => e?.GetValue<string>() ?? "").ToList()
};
}
var type = param["type"]?.GetValue<string>() ?? "string";
if (type == "array")
{
return new SchemaModel
{
Type = "array",
Items = param["items"] is JsonNode items ? ParseInlineSchema2((JsonObject)items) : null
};
}
return new SchemaModel
{
Type = type,
Format = param["format"]?.GetValue<string>()
};
}
private static SchemaModel ParseSchema2(JsonNode node)
{
if (node is not JsonObject obj)
{
return new SchemaModel { Type = "object" };
}
if (obj["$ref"]?.GetValue<string>() is string refVal)
{
return new SchemaModel { Type = "object", RefName = refVal.Split('/').Last() };
}
if (obj["enum"] is JsonArray enumArray)
{
return new SchemaModel
{
Type = "enum",
EnumValues = enumArray.Select(e => e?.GetValue<string>() ?? "").ToList()
};
}
var type = obj["type"]?.GetValue<string>() ?? "object";
if (type == "array")
{
return new SchemaModel
{
Type = "array",
Items = obj["items"] is JsonNode items ? ParseSchema2(items) : null
};
}
if (type == "object")
{
Dictionary<string, SchemaModel>? props = null;
if (obj["properties"] is JsonObject propsNode)
{
props = new Dictionary<string, SchemaModel>();
foreach (var (name, propNode) in propsNode)
{
if (propNode is not null)
{
props[name] = ParseSchema2(propNode);
}
}
}
SchemaModel? dictItems = null;
if (obj["additionalProperties"] is JsonNode addProps)
{
dictItems = ParseSchema2(addProps);
}
return new SchemaModel
{
Type = "object",
Properties = props,
Required = ParseStringList(obj["required"]),
Items = dictItems
};
}
return new SchemaModel
{
Type = type,
Format = obj["format"]?.GetValue<string>()
};
}
private static (string contentType, JsonObject? mediaObj) PickMediaType(JsonObject? content)
{
if (content is null)
{
return ("application/json", null);
}
if (content["application/json"] is JsonObject json)
{
return ("application/json", json);
}
foreach (var (ct, node) in content)
{
if (node is JsonObject obj)
{
return (ct, obj);
}
}
return ("application/json", null);
}
private static List<string> ParseSecurityArray(JsonNode? node)
{
var list = new List<string>();
if (node is not JsonArray arr)
{
return list;
}
foreach (var requirement in arr)
{
if (requirement is JsonObject reqObj)
{
foreach (var (schemeId, _) in reqObj)
{
list.Add(schemeId);
}
}
}
return list;
}
private static SecuritySchemeModel ParseSecurityScheme2(string id, JsonObject obj)
{
var type = obj["type"]?.GetValue<string>() ?? "apiKey";
var scheme = new SecuritySchemeModel
{
Id = id,
Description = obj["description"]?.GetValue<string>()
};
if (type == "basic")
{
scheme.Type = "http";
scheme.Scheme = "basic";
}
else if (type == "apiKey")
{
scheme.Type = "apiKey";
scheme.Name = obj["name"]?.GetValue<string>();
scheme.In = obj["in"]?.GetValue<string>();
}
else if (type == "oauth2")
{
scheme.Type = "oauth2";
var flow = obj["flow"]?.GetValue<string>();
var flowModel = new OAuthFlowModel
{
AuthorizationUrl = obj["authorizationUrl"]?.GetValue<string>(),
TokenUrl = obj["tokenUrl"]?.GetValue<string>()
};
if (obj["scopes"] is JsonObject scopesObj)
{
foreach (var (k, v) in scopesObj)
{
flowModel.Scopes[k] = v?.GetValue<string>() ?? "";
}
}
scheme.Flows = flow switch
{
"application" => new OAuthFlowsModel { ClientCredentials = flowModel },
"accessCode" => new OAuthFlowsModel { AuthorizationCode = flowModel },
"password" => new OAuthFlowsModel { Password = flowModel },
_ => new OAuthFlowsModel { Implicit = flowModel }
};
}
else
{
scheme.Type = type;
}
return scheme;
}
private static SecuritySchemeModel ParseSecurityScheme3(string id, JsonObject obj)
{
var scheme = new SecuritySchemeModel
{
Id = id,
Type = obj["type"]?.GetValue<string>() ?? "apiKey",
Description = obj["description"]?.GetValue<string>()
};
switch (scheme.Type)
{
case "apiKey":
scheme.Name = obj["name"]?.GetValue<string>();
scheme.In = obj["in"]?.GetValue<string>();
break;
case "http":
scheme.Scheme = obj["scheme"]?.GetValue<string>();
scheme.BearerFormat = obj["bearerFormat"]?.GetValue<string>();
break;
case "oauth2":
if (obj["flows"] is JsonObject flowsObj)
{
scheme.Flows = new OAuthFlowsModel
{
ClientCredentials = ParseOAuthFlow3(flowsObj["clientCredentials"] as JsonObject),
AuthorizationCode = ParseOAuthFlow3(flowsObj["authorizationCode"] as JsonObject),
Password = ParseOAuthFlow3(flowsObj["password"] as JsonObject),
Implicit = ParseOAuthFlow3(flowsObj["implicit"] as JsonObject)
};
}
break;
}
return scheme;
}
private static OAuthFlowModel? ParseOAuthFlow3(JsonObject? obj)
{
if (obj is null)
{
return null;
}
var flow = new OAuthFlowModel
{
AuthorizationUrl = obj["authorizationUrl"]?.GetValue<string>(),
TokenUrl = obj["tokenUrl"]?.GetValue<string>(),
RefreshUrl = obj["refreshUrl"]?.GetValue<string>()
};
if (obj["scopes"] is JsonObject scopesObj)
{
foreach (var (k, v) in scopesObj)
{
flow.Scopes[k] = v?.GetValue<string>() ?? "";
}
}
return flow;
}
private static List<string> ParseStringArray(JsonNode? node)
{
var list = new List<string>();
if (node is JsonArray arr)
{
foreach (var item in arr)
{
if (item?.GetValue<string>() is string s)
{
list.Add(s);
}
}
}
return list;
}
private static List<string>? ParseStringList(JsonNode? node)
{
if (node is not JsonArray arr || arr.Count == 0)
{
return null;
}
return arr.Select(e => e?.GetValue<string>() ?? "").ToList();
}
private static bool IsHttpMethod(string s) =>
s is "get" or "post" or "put" or "patch" or "delete" or "head" or "options" or "trace";
private static string SanitizePath(string path)
=> path.Trim('/').Replace('/', '_').Replace('{', '_').Replace('}', '_');
}
}