Initial version
This commit is contained in:
@@ -0,0 +1,603 @@
|
||||
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('}', '_');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user