using System.Text.Json; using System.Text.Json.Nodes; using EonaCat.DoxaApi.Models; namespace EonaCat.DoxaApi.Interop { 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); } } } 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"]) }; if (op["parameters"] is JsonArray parameters) { foreach (var p in parameters) { if (p is not JsonObject param) { continue; } var schema = p["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); } } } 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"]) }; 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 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('}', '_'); } }