604 lines
20 KiB
C#
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('}', '_');
|
|
}
|
|
}
|