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 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(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() ?? "API"; doc.Info.Version = info["version"]?.GetValue() ?? "v1"; doc.Info.Description = info["description"]?.GetValue(); } if (root["servers"] is JsonArray servers) { foreach (var s in servers) { if (s?["url"]?.GetValue() 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(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() ?? $"{method}_{SanitizePath(path)}", Summary = op["summary"]?.GetValue(), Description = op["description"]?.GetValue(), Method = method, Path = path, Deprecated = op["deprecated"]?.GetValue() ?? 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() ?? "", In = param["in"]?.GetValue() ?? "query", Required = param["required"]?.GetValue() ?? false, Description = param["description"]?.GetValue(), Schema = schema }); } } if (op["requestBody"] is JsonObject rb) { var required = rb["required"]?.GetValue() ?? 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(), 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() is string refVal) { var refName = refVal.Split('/').Last(); return new SchemaModel { Type = "object", RefName = refName }; } bool nullable = obj["nullable"]?.GetValue() ?? false; if (obj["enum"] is JsonArray enumArray) { return new SchemaModel { Type = "enum", EnumValues = enumArray.Select(e => e?.GetValue() ?? "").ToList(), Nullable = nullable }; } var type = obj["type"]?.GetValue() ?? "object"; if (type == "array") { return new SchemaModel { Type = "array", Items = obj["items"] is JsonNode items ? ParseSchema3(items) : null, Nullable = nullable }; } if (type == "object") { Dictionary? props = null; if (obj["properties"] is JsonObject propsNode) { props = new Dictionary(); 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(), Nullable = nullable }; } private static ApiDocument ImportSwagger2(JsonNode root) { var doc = new ApiDocument(); if (root["info"] is JsonObject info) { doc.Info.Title = info["title"]?.GetValue() ?? "API"; doc.Info.Version = info["version"]?.GetValue() ?? "v1"; doc.Info.Description = info["description"]?.GetValue(); } var host = root["host"]?.GetValue(); var basePath = root["basePath"]?.GetValue() ?? "/"; var scheme = root["schemes"] is JsonArray schemes && schemes.Count > 0 ? schemes[0]?.GetValue() ?? "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(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() ?? $"{method}_{SanitizePath(path)}", Summary = op["summary"]?.GetValue(), Description = op["description"]?.GetValue(), Method = method, Path = path, Deprecated = op["deprecated"]?.GetValue() ?? 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() ?? "query"; if (inLoc == "body") { endpoint.RequestBody = new RequestBodyModel { Required = param["required"]?.GetValue() ?? 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() ?? "", In = inLoc, Required = param["required"]?.GetValue() ?? false, Description = param["description"]?.GetValue(), 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(), Schema = schema }); } } return endpoint; } private static SchemaModel ParseInlineSchema2(JsonObject param) { if (param["$ref"]?.GetValue() 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() ?? "").ToList() }; } var type = param["type"]?.GetValue() ?? "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() }; } private static SchemaModel ParseSchema2(JsonNode node) { if (node is not JsonObject obj) { return new SchemaModel { Type = "object" }; } if (obj["$ref"]?.GetValue() 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() ?? "").ToList() }; } var type = obj["type"]?.GetValue() ?? "object"; if (type == "array") { return new SchemaModel { Type = "array", Items = obj["items"] is JsonNode items ? ParseSchema2(items) : null }; } if (type == "object") { Dictionary? props = null; if (obj["properties"] is JsonObject propsNode) { props = new Dictionary(); 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() }; } 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 ParseSecurityArray(JsonNode? node) { var list = new List(); 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() ?? "apiKey"; var scheme = new SecuritySchemeModel { Id = id, Description = obj["description"]?.GetValue() }; if (type == "basic") { scheme.Type = "http"; scheme.Scheme = "basic"; } else if (type == "apiKey") { scheme.Type = "apiKey"; scheme.Name = obj["name"]?.GetValue(); scheme.In = obj["in"]?.GetValue(); } else if (type == "oauth2") { scheme.Type = "oauth2"; var flow = obj["flow"]?.GetValue(); var flowModel = new OAuthFlowModel { AuthorizationUrl = obj["authorizationUrl"]?.GetValue(), TokenUrl = obj["tokenUrl"]?.GetValue() }; if (obj["scopes"] is JsonObject scopesObj) { foreach (var (k, v) in scopesObj) { flowModel.Scopes[k] = v?.GetValue() ?? ""; } } 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() ?? "apiKey", Description = obj["description"]?.GetValue() }; switch (scheme.Type) { case "apiKey": scheme.Name = obj["name"]?.GetValue(); scheme.In = obj["in"]?.GetValue(); break; case "http": scheme.Scheme = obj["scheme"]?.GetValue(); scheme.BearerFormat = obj["bearerFormat"]?.GetValue(); 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(), TokenUrl = obj["tokenUrl"]?.GetValue(), RefreshUrl = obj["refreshUrl"]?.GetValue() }; if (obj["scopes"] is JsonObject scopesObj) { foreach (var (k, v) in scopesObj) { flow.Scopes[k] = v?.GetValue() ?? ""; } } return flow; } private static List ParseStringArray(JsonNode? node) { var list = new List(); if (node is JsonArray arr) { foreach (var item in arr) { if (item?.GetValue() is string s) { list.Add(s); } } } return list; } private static List? ParseStringList(JsonNode? node) { if (node is not JsonArray arr || arr.Count == 0) { return null; } return arr.Select(e => e?.GetValue() ?? "").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('}', '_'); } }