928 lines
23 KiB
C#
928 lines
23 KiB
C#
using EonaCat.DoxaApi.Models;
|
|
using EonaCat.Json;
|
|
using EonaCat.Json.Linq;
|
|
|
|
namespace EonaCat.DoxaApi.Interop
|
|
{
|
|
public static class OpenApiImporter
|
|
{
|
|
public static ApiDocument Import(string json)
|
|
{
|
|
var node = JToken.Parse(json);
|
|
|
|
return Import(node);
|
|
}
|
|
|
|
public static async Task<ApiDocument> ImportAsync(Stream stream)
|
|
{
|
|
using var reader = new StreamReader(stream);
|
|
|
|
var json = await reader.ReadToEndAsync();
|
|
|
|
return Import(JToken.Parse(json));
|
|
}
|
|
|
|
public static ApiDocument Import(JToken root)
|
|
{
|
|
if (root["openapi"] != null)
|
|
{
|
|
return ImportOpenApi3(root);
|
|
}
|
|
|
|
if (root["swagger"] != null)
|
|
{
|
|
return ImportSwagger2(root);
|
|
}
|
|
|
|
if (root["groups"] != null &&
|
|
root["info"] != null)
|
|
{
|
|
return DoxaApiImporter.Import(
|
|
root.ToString(Formatting.None));
|
|
}
|
|
|
|
throw new NotSupportedException(
|
|
"Supported formats: DoxaApi, OpenAPI 3.x, Swagger 2.0");
|
|
}
|
|
|
|
private static ApiDocument ImportOpenApi3(JToken root)
|
|
{
|
|
var doc = new ApiDocument();
|
|
|
|
if (root["info"] is JObject info)
|
|
{
|
|
doc.Info.Title =
|
|
info["title"]?.Value<string>() ?? "API";
|
|
|
|
doc.Info.Version =
|
|
info["version"]?.Value<string>() ?? "v1";
|
|
|
|
doc.Info.Description =
|
|
info["description"]?.Value<string>();
|
|
}
|
|
|
|
if (root["servers"] is JArray servers)
|
|
{
|
|
foreach (var s in servers)
|
|
{
|
|
var url = s["url"]?.Value<string>();
|
|
|
|
if (url != null)
|
|
{
|
|
doc.Servers.Add(url);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (root["components"]?["schemas"] is JObject schemas)
|
|
{
|
|
foreach (var item in schemas.Properties())
|
|
{
|
|
doc.Schemas[item.Name] =
|
|
ParseSchema3(item.Value);
|
|
}
|
|
}
|
|
|
|
if (root["components"]?["securitySchemes"] is JObject security)
|
|
{
|
|
foreach (var item in security.Properties())
|
|
{
|
|
if (item.Value is JObject scheme)
|
|
{
|
|
doc.SecuritySchemes[item.Name] =
|
|
ParseSecurityScheme3(
|
|
item.Name,
|
|
scheme);
|
|
}
|
|
}
|
|
}
|
|
|
|
var groups =
|
|
new Dictionary<string, ApiGroup>(
|
|
StringComparer.OrdinalIgnoreCase);
|
|
|
|
if (root["paths"] is JObject paths)
|
|
{
|
|
foreach (var path in paths.Properties())
|
|
{
|
|
if (path.Value is not JObject pathItem)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var method in pathItem.Properties())
|
|
{
|
|
if (!IsHttpMethod(method.Name))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (method.Value is not JObject op)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var endpoint =
|
|
ParseOperation3(
|
|
op,
|
|
path.Name,
|
|
method.Name.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(
|
|
x => x.Name,
|
|
StringComparer.OrdinalIgnoreCase)
|
|
.ToList();
|
|
|
|
return doc;
|
|
}
|
|
|
|
private static ApiEndpoint ParseOperation3(
|
|
JObject op,
|
|
string path,
|
|
string method)
|
|
{
|
|
var endpoint = new ApiEndpoint
|
|
{
|
|
OperationId =
|
|
op["operationId"]?.Value<string>()
|
|
?? $"{method}_{SanitizePath(path)}",
|
|
|
|
Summary =
|
|
op["summary"]?.Value<string>(),
|
|
|
|
Description =
|
|
op["description"]?.Value<string>(),
|
|
|
|
Method = method,
|
|
Path = path,
|
|
|
|
Deprecated =
|
|
op["deprecated"]?.Value<bool>()
|
|
?? false,
|
|
|
|
Tags = ParseStringArray(op["tags"]),
|
|
Security = ParseSecurityArray(op["security"])
|
|
};
|
|
|
|
|
|
if (op["parameters"] is JArray parameters)
|
|
{
|
|
foreach (var item in parameters)
|
|
{
|
|
if (item is not JObject param)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
|
|
endpoint.Parameters.Add(new ApiParameter
|
|
{
|
|
Name =
|
|
param["name"]?.Value<string>()
|
|
?? "",
|
|
|
|
In =
|
|
param["in"]?.Value<string>()
|
|
?? "query",
|
|
|
|
Required =
|
|
param["required"]?.Value<bool>()
|
|
?? false,
|
|
|
|
Description =
|
|
param["description"]?.Value<string>(),
|
|
|
|
Schema =
|
|
param["schema"] != null
|
|
? ParseSchema3(param["schema"])
|
|
: new SchemaModel
|
|
{
|
|
Type = "string"
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
if (op["requestBody"] is JObject rb)
|
|
{
|
|
var content =
|
|
rb["content"] as JObject;
|
|
|
|
|
|
var picked =
|
|
PickMediaType(content);
|
|
|
|
|
|
SchemaModel schema;
|
|
|
|
string? example = null;
|
|
|
|
|
|
if (picked.mediaObj != null)
|
|
{
|
|
schema =
|
|
picked.mediaObj["schema"] != null
|
|
? ParseSchema3(
|
|
picked.mediaObj["schema"])
|
|
: new SchemaModel
|
|
{
|
|
Type = "object"
|
|
};
|
|
|
|
|
|
example =
|
|
picked.mediaObj["example"]
|
|
?.ToString(Formatting.None);
|
|
}
|
|
else
|
|
{
|
|
schema = new SchemaModel
|
|
{
|
|
Type = "object"
|
|
};
|
|
}
|
|
|
|
|
|
endpoint.RequestBody =
|
|
new RequestBodyModel
|
|
{
|
|
Required =
|
|
rb["required"]?.Value<bool>()
|
|
?? false,
|
|
|
|
ContentType =
|
|
picked.contentType,
|
|
|
|
Schema = schema,
|
|
|
|
Example = example
|
|
};
|
|
}
|
|
|
|
|
|
if (op["responses"] is JObject responses)
|
|
{
|
|
foreach (var response in responses.Properties())
|
|
{
|
|
if (response.Value is not JObject resp)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
|
|
SchemaModel? schema = null;
|
|
|
|
|
|
if (resp["content"] is JObject content)
|
|
{
|
|
var picked =
|
|
PickMediaType(content);
|
|
|
|
|
|
if (picked.mediaObj?["schema"] != null)
|
|
{
|
|
schema =
|
|
ParseSchema3(
|
|
picked.mediaObj["schema"]);
|
|
}
|
|
}
|
|
|
|
|
|
endpoint.Responses.Add(
|
|
new ResponseModel
|
|
{
|
|
StatusCode =
|
|
response.Name,
|
|
|
|
Description =
|
|
resp["description"]
|
|
?.Value<string>(),
|
|
|
|
Schema = schema
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
return endpoint;
|
|
}
|
|
|
|
|
|
private static SchemaModel ParseSchema3(JToken node)
|
|
{
|
|
if (node is not JObject obj)
|
|
{
|
|
return new SchemaModel
|
|
{
|
|
Type = "object"
|
|
};
|
|
}
|
|
|
|
|
|
if (obj["$ref"]?.Value<string>() is string refValue)
|
|
{
|
|
return new SchemaModel
|
|
{
|
|
Type = "object",
|
|
RefName =
|
|
refValue.Split('/').Last()
|
|
};
|
|
}
|
|
|
|
|
|
var nullable =
|
|
obj["nullable"]?.Value<bool>()
|
|
?? false;
|
|
|
|
|
|
if (obj["enum"] is JArray enums)
|
|
{
|
|
return new SchemaModel
|
|
{
|
|
Type = "enum",
|
|
|
|
EnumValues =
|
|
enums
|
|
.Select(x =>
|
|
x?.Value<string>() ?? "")
|
|
.ToList(),
|
|
|
|
Nullable = nullable
|
|
};
|
|
}
|
|
|
|
|
|
var type =
|
|
obj["type"]?.Value<string>()
|
|
?? "object";
|
|
|
|
|
|
if (type == "array")
|
|
{
|
|
return new SchemaModel
|
|
{
|
|
Type = "array",
|
|
|
|
Items =
|
|
obj["items"] != null
|
|
? ParseSchema3(
|
|
obj["items"])
|
|
: null,
|
|
|
|
Nullable = nullable
|
|
};
|
|
}
|
|
|
|
|
|
if (type == "object")
|
|
{
|
|
Dictionary<string, SchemaModel>? props = null;
|
|
|
|
|
|
if (obj["properties"] is JObject properties)
|
|
{
|
|
props =
|
|
new Dictionary<string, SchemaModel>();
|
|
|
|
foreach (var property in properties.Properties())
|
|
{
|
|
props[property.Name] =
|
|
ParseSchema3(
|
|
property.Value);
|
|
}
|
|
}
|
|
|
|
|
|
SchemaModel? dictionaryItems = null;
|
|
|
|
|
|
if (obj["additionalProperties"] != null)
|
|
{
|
|
dictionaryItems =
|
|
ParseSchema3(
|
|
obj["additionalProperties"]);
|
|
}
|
|
|
|
|
|
return new SchemaModel
|
|
{
|
|
Type = "object",
|
|
|
|
Properties = props,
|
|
|
|
Required =
|
|
ParseStringList(
|
|
obj["required"]),
|
|
|
|
Items = dictionaryItems,
|
|
|
|
Nullable = nullable
|
|
};
|
|
}
|
|
|
|
|
|
return new SchemaModel
|
|
{
|
|
Type = type,
|
|
|
|
Format =
|
|
obj["format"]?.Value<string>(),
|
|
|
|
Nullable = nullable
|
|
};
|
|
}
|
|
|
|
|
|
private static ApiDocument ImportSwagger2(JToken root)
|
|
{
|
|
var doc = new ApiDocument();
|
|
|
|
|
|
if (root["info"] is JObject info)
|
|
{
|
|
doc.Info.Title =
|
|
info["title"]?.Value<string>()
|
|
?? "API";
|
|
|
|
doc.Info.Version =
|
|
info["version"]?.Value<string>()
|
|
?? "v1";
|
|
|
|
doc.Info.Description =
|
|
info["description"]?.Value<string>();
|
|
}
|
|
|
|
|
|
var host =
|
|
root["host"]?.Value<string>();
|
|
|
|
var basePath =
|
|
root["basePath"]?.Value<string>()
|
|
?? "/";
|
|
|
|
|
|
var scheme =
|
|
root["schemes"] is JArray schemes
|
|
&& schemes.Count > 0
|
|
? schemes[0]?.Value<string>()
|
|
?? "https"
|
|
: "https";
|
|
|
|
|
|
if (host != null)
|
|
{
|
|
doc.Servers.Add(
|
|
$"{scheme}://{host}{basePath.TrimEnd('/')}");
|
|
}
|
|
|
|
|
|
if (root["definitions"] is JObject definitions)
|
|
{
|
|
foreach (var item in definitions.Properties())
|
|
{
|
|
doc.Schemas[item.Name] =
|
|
ParseSchema2(item.Value);
|
|
}
|
|
}
|
|
|
|
|
|
if (root["securityDefinitions"] is JObject security)
|
|
{
|
|
foreach (var item in security.Properties())
|
|
{
|
|
if (item.Value is JObject obj)
|
|
{
|
|
doc.SecuritySchemes[item.Name] =
|
|
ParseSecurityScheme2(
|
|
item.Name,
|
|
obj);
|
|
}
|
|
}
|
|
}
|
|
|
|
return doc;
|
|
}
|
|
|
|
|
|
private static SchemaModel ParseSchema2(JToken node)
|
|
{
|
|
if (node is not JObject obj)
|
|
{
|
|
return new SchemaModel
|
|
{
|
|
Type = "object"
|
|
};
|
|
}
|
|
|
|
|
|
if (obj["$ref"]?.Value<string>() is string refValue)
|
|
{
|
|
return new SchemaModel
|
|
{
|
|
Type = "object",
|
|
RefName = refValue.Split('/').Last()
|
|
};
|
|
}
|
|
|
|
|
|
if (obj["enum"] is JArray enums)
|
|
{
|
|
return new SchemaModel
|
|
{
|
|
Type = "enum",
|
|
|
|
EnumValues =
|
|
enums.Select(x =>
|
|
x?.Value<string>() ?? "")
|
|
.ToList()
|
|
};
|
|
}
|
|
|
|
|
|
var type =
|
|
obj["type"]?.Value<string>()
|
|
?? "object";
|
|
|
|
|
|
if (type == "array")
|
|
{
|
|
return new SchemaModel
|
|
{
|
|
Type = "array",
|
|
|
|
Items =
|
|
obj["items"] != null
|
|
? ParseSchema2(obj["items"])
|
|
: null
|
|
};
|
|
}
|
|
|
|
|
|
if (type == "object")
|
|
{
|
|
Dictionary<string, SchemaModel>? props = null;
|
|
|
|
|
|
if (obj["properties"] is JObject properties)
|
|
{
|
|
props =
|
|
new Dictionary<string, SchemaModel>();
|
|
|
|
foreach (var property in properties.Properties())
|
|
{
|
|
props[property.Name] =
|
|
ParseSchema2(property.Value);
|
|
}
|
|
}
|
|
|
|
|
|
SchemaModel? dictionaryItems = null;
|
|
|
|
|
|
if (obj["additionalProperties"] != null)
|
|
{
|
|
dictionaryItems =
|
|
ParseSchema2(
|
|
obj["additionalProperties"]);
|
|
}
|
|
|
|
|
|
return new SchemaModel
|
|
{
|
|
Type = "object",
|
|
|
|
Properties = props,
|
|
|
|
Required =
|
|
ParseStringList(
|
|
obj["required"]),
|
|
|
|
Items = dictionaryItems
|
|
};
|
|
}
|
|
|
|
|
|
return new SchemaModel
|
|
{
|
|
Type = type,
|
|
|
|
Format =
|
|
obj["format"]?.Value<string>()
|
|
};
|
|
}
|
|
|
|
|
|
private static (string contentType, JObject? mediaObj)
|
|
PickMediaType(JObject? content)
|
|
{
|
|
if (content == null)
|
|
{
|
|
return ("application/json", null);
|
|
}
|
|
|
|
|
|
if (content["application/json"] is JObject json)
|
|
{
|
|
return ("application/json", json);
|
|
}
|
|
|
|
|
|
foreach (var item in content.Properties())
|
|
{
|
|
if (item.Value is JObject obj)
|
|
{
|
|
return (item.Name, obj);
|
|
}
|
|
}
|
|
|
|
|
|
return ("application/json", null);
|
|
}
|
|
|
|
|
|
private static List<string> ParseSecurityArray(
|
|
JToken? node)
|
|
{
|
|
var result = new List<string>();
|
|
|
|
|
|
if (node is not JArray arr)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
|
|
foreach (var item in arr)
|
|
{
|
|
if (item is JObject obj)
|
|
{
|
|
foreach (var prop in obj.Properties())
|
|
{
|
|
result.Add(prop.Name);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
private static SecuritySchemeModel ParseSecurityScheme2(
|
|
string id,
|
|
JObject obj)
|
|
{
|
|
var type =
|
|
obj["type"]?.Value<string>()
|
|
?? "apiKey";
|
|
|
|
|
|
var scheme =
|
|
new SecuritySchemeModel
|
|
{
|
|
Id = id,
|
|
|
|
Description =
|
|
obj["description"]
|
|
?.Value<string>()
|
|
};
|
|
|
|
|
|
if (type == "basic")
|
|
{
|
|
scheme.Type = "http";
|
|
scheme.Scheme = "basic";
|
|
}
|
|
else if (type == "apiKey")
|
|
{
|
|
scheme.Type = "apiKey";
|
|
|
|
scheme.Name =
|
|
obj["name"]?.Value<string>();
|
|
|
|
scheme.In =
|
|
obj["in"]?.Value<string>();
|
|
}
|
|
else
|
|
{
|
|
scheme.Type = type;
|
|
}
|
|
|
|
|
|
return scheme;
|
|
}
|
|
|
|
|
|
private static SecuritySchemeModel ParseSecurityScheme3(
|
|
string id,
|
|
JObject obj)
|
|
{
|
|
var scheme =
|
|
new SecuritySchemeModel
|
|
{
|
|
Id = id,
|
|
|
|
Type =
|
|
obj["type"]?.Value<string>()
|
|
?? "apiKey",
|
|
|
|
Description =
|
|
obj["description"]
|
|
?.Value<string>()
|
|
};
|
|
|
|
|
|
switch (scheme.Type)
|
|
{
|
|
case "apiKey":
|
|
|
|
scheme.Name =
|
|
obj["name"]?.Value<string>();
|
|
|
|
scheme.In =
|
|
obj["in"]?.Value<string>();
|
|
|
|
break;
|
|
|
|
|
|
case "http":
|
|
|
|
scheme.Scheme =
|
|
obj["scheme"]
|
|
?.Value<string>();
|
|
|
|
scheme.BearerFormat =
|
|
obj["bearerFormat"]
|
|
?.Value<string>();
|
|
|
|
break;
|
|
|
|
|
|
case "oauth2":
|
|
|
|
if (obj["flows"] is JObject flows)
|
|
{
|
|
scheme.Flows =
|
|
new OAuthFlowsModel
|
|
{
|
|
ClientCredentials =
|
|
ParseOAuthFlow3(
|
|
flows["clientCredentials"]
|
|
as JObject),
|
|
|
|
AuthorizationCode =
|
|
ParseOAuthFlow3(
|
|
flows["authorizationCode"]
|
|
as JObject),
|
|
|
|
Password =
|
|
ParseOAuthFlow3(
|
|
flows["password"]
|
|
as JObject),
|
|
|
|
Implicit =
|
|
ParseOAuthFlow3(
|
|
flows["implicit"]
|
|
as JObject)
|
|
};
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
|
|
return scheme;
|
|
}
|
|
|
|
|
|
private static OAuthFlowModel? ParseOAuthFlow3(
|
|
JObject? obj)
|
|
{
|
|
if (obj == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
|
|
var flow =
|
|
new OAuthFlowModel
|
|
{
|
|
AuthorizationUrl =
|
|
obj["authorizationUrl"]
|
|
?.Value<string>(),
|
|
|
|
TokenUrl =
|
|
obj["tokenUrl"]
|
|
?.Value<string>(),
|
|
|
|
RefreshUrl =
|
|
obj["refreshUrl"]
|
|
?.Value<string>()
|
|
};
|
|
|
|
|
|
if (obj["scopes"] is JObject scopes)
|
|
{
|
|
foreach (var item in scopes.Properties())
|
|
{
|
|
flow.Scopes[item.Name] =
|
|
item.Value.Value<string>()
|
|
?? "";
|
|
}
|
|
}
|
|
|
|
|
|
return flow;
|
|
}
|
|
|
|
|
|
private static List<string> ParseStringArray(
|
|
JToken? node)
|
|
{
|
|
var list = new List<string>();
|
|
|
|
|
|
if (node is JArray arr)
|
|
{
|
|
foreach (var item in arr)
|
|
{
|
|
var value =
|
|
item.Value<string>();
|
|
|
|
if (value != null)
|
|
{
|
|
list.Add(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
return list;
|
|
}
|
|
|
|
|
|
private static List<string>? ParseStringList(
|
|
JToken? node)
|
|
{
|
|
if (node is not JArray arr ||
|
|
arr.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
|
|
return arr
|
|
.Select(x =>
|
|
x.Value<string>() ?? "")
|
|
.ToList();
|
|
}
|
|
|
|
|
|
private static bool IsHttpMethod(string s)
|
|
{
|
|
return s is
|
|
"get" or
|
|
"post" or
|
|
"put" or
|
|
"patch" or
|
|
"delete" or
|
|
"head" or
|
|
"options" or
|
|
"trace";
|
|
}
|
|
|
|
|
|
private static string SanitizePath(string path)
|
|
{
|
|
return path
|
|
.Trim('/')
|
|
.Replace('/', '_')
|
|
.Replace('{', '_')
|
|
.Replace('}', '_');
|
|
}
|
|
}
|
|
} |