Files
2026-06-20 10:26:27 +02:00

604 lines
20 KiB
C#

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<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);
}
}
}
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"])
};
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<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);
}
}
}
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"])
};
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> 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('}', '_');
}
}