Загрузка данных
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
namespace CodeBase.EditorTools
{
public sealed class SwaggerDtoToCSharpConverterWindow : EditorWindow
{
private const string DefaultRootClassName = "RootDto";
private string _inputJson = string.Empty;
private string _outputCode = string.Empty;
private Vector2 _inputScrollPosition;
private Vector2 _outputScrollPosition;
[MenuItem("Tools/Swagger DTO To C#")]
public static void Open()
{
SwaggerDtoToCSharpConverterWindow window = GetWindow<SwaggerDtoToCSharpConverterWindow>("Swagger DTO To C#");
window.minSize = new Vector2(800f, 600f);
window.Show();
}
private void OnGUI()
{
EditorGUILayout.LabelField("Swagger DTO JSON", EditorStyles.boldLabel);
_inputScrollPosition = EditorGUILayout.BeginScrollView(
_inputScrollPosition,
GUILayout.MinHeight(220f),
GUILayout.ExpandHeight(true));
_inputJson = EditorGUILayout.TextArea(_inputJson, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
EditorGUILayout.Space(8f);
if (GUILayout.Button("Convert", GUILayout.Height(32f)))
{
Convert();
}
EditorGUILayout.Space(8f);
EditorGUILayout.LabelField("C# Class", EditorStyles.boldLabel);
_outputScrollPosition = EditorGUILayout.BeginScrollView(
_outputScrollPosition,
GUILayout.MinHeight(260f),
GUILayout.ExpandHeight(true));
_outputCode = EditorGUILayout.TextArea(_outputCode, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
}
private void Convert()
{
if (string.IsNullOrWhiteSpace(_inputJson))
{
_outputCode = "// Input is empty.";
return;
}
try
{
SwaggerDtoCSharpClassGenerator generator = new SwaggerDtoCSharpClassGenerator();
_outputCode = generator.Generate(_inputJson, DefaultRootClassName);
}
catch (Exception exception)
{
_outputCode = $"// Convert failed:\n// {exception.Message}";
}
}
}
internal sealed class SwaggerDtoCSharpClassGenerator
{
private readonly List<ClassDefinition> _classDefinitions = new List<ClassDefinition>();
private readonly Dictionary<string, JObject> _schemasByName = new Dictionary<string, JObject>();
private readonly Dictionary<string, string> _generatedClassNamesBySchemaName = new Dictionary<string, string>();
private readonly HashSet<string> _usedClassNames = new HashSet<string>();
public string Generate(string json, string defaultRootClassName)
{
_classDefinitions.Clear();
_schemasByName.Clear();
_generatedClassNamesBySchemaName.Clear();
_usedClassNames.Clear();
JObject rootObject = ParseInput(json);
JObject schemasObject = ExtractSchemasObject(rootObject);
if (schemasObject != null)
{
RegisterSchemas(schemasObject);
foreach (JProperty schemaProperty in schemasObject.Properties())
{
if (schemaProperty.Value is JObject schemaObject)
{
BuildClassFromSchema(schemaProperty.Name, schemaObject);
}
}
}
else if (LooksLikeSingleSchema(rootObject))
{
BuildClassFromSchema(defaultRootClassName, rootObject);
}
else
{
RegisterSchemas(rootObject);
foreach (JProperty schemaProperty in rootObject.Properties())
{
if (schemaProperty.Value is JObject schemaObject && LooksLikeSingleSchema(schemaObject))
{
BuildClassFromSchema(schemaProperty.Name, schemaObject);
}
}
}
return BuildOutputCode();
}
private JObject ParseInput(string json)
{
string trimmedJson = json.Trim();
try
{
return JObject.Parse(trimmedJson);
}
catch
{
if (trimmedJson.StartsWith("\"", StringComparison.Ordinal))
{
return JObject.Parse("{" + trimmedJson.TrimEnd(',') + "}");
}
throw;
}
}
private JObject ExtractSchemasObject(JObject rootObject)
{
JToken componentsSchemasToken = rootObject["components"]?["schemas"];
if (componentsSchemasToken is JObject componentsSchemasObject)
{
return componentsSchemasObject;
}
JToken definitionsToken = rootObject["definitions"];
if (definitionsToken is JObject definitionsObject)
{
return definitionsObject;
}
return null;
}
private void RegisterSchemas(JObject schemasObject)
{
foreach (JProperty schemaProperty in schemasObject.Properties())
{
if (schemaProperty.Value is JObject schemaObject)
{
_schemasByName[schemaProperty.Name] = schemaObject;
}
}
}
private bool LooksLikeSingleSchema(JObject objectValue)
{
return objectValue["properties"] is JObject ||
objectValue["type"] != null ||
objectValue["allOf"] is JArray ||
objectValue["$ref"] != null;
}
private string BuildClassFromSchema(string suggestedClassName, JObject schemaObject)
{
string normalizedSuggestedClassName = NormalizeClassName(suggestedClassName);
if (_generatedClassNamesBySchemaName.TryGetValue(suggestedClassName, out string existingClassName))
{
return existingClassName;
}
string className = GetUniqueClassName(normalizedSuggestedClassName);
_generatedClassNamesBySchemaName[suggestedClassName] = className;
ClassDefinition classDefinition = new ClassDefinition(className);
_classDefinitions.Add(classDefinition);
JObject propertiesObject = GetMergedProperties(schemaObject);
if (propertiesObject == null)
{
return className;
}
HashSet<string> usedFieldNames = new HashSet<string>();
foreach (JProperty property in propertiesObject.Properties())
{
if (property.Value is not JObject propertySchemaObject)
{
continue;
}
string fieldName = GetUniqueFieldName(ToPascalCase(property.Name), usedFieldNames);
string fieldTypeName = ResolveType(property.Name, propertySchemaObject);
classDefinition.Fields.Add(new FieldDefinition(fieldTypeName, fieldName));
}
return className;
}
private JObject GetMergedProperties(JObject schemaObject)
{
JObject result = new JObject();
if (schemaObject["allOf"] is JArray allOfArray)
{
foreach (JToken itemToken in allOfArray)
{
if (itemToken is not JObject itemObject)
{
continue;
}
JObject resolvedObject = ResolveSchemaObject(itemObject);
JObject itemPropertiesObject = GetMergedProperties(resolvedObject);
if (itemPropertiesObject == null)
{
continue;
}
foreach (JProperty property in itemPropertiesObject.Properties())
{
result[property.Name] = property.Value;
}
}
}
if (schemaObject["properties"] is JObject propertiesObject)
{
foreach (JProperty property in propertiesObject.Properties())
{
result[property.Name] = property.Value;
}
}
return result.Properties().Any() ? result : null;
}
private JObject ResolveSchemaObject(JObject schemaObject)
{
string referencePath = schemaObject.Value<string>("$ref");
if (string.IsNullOrWhiteSpace(referencePath))
{
return schemaObject;
}
string schemaName = GetSchemaNameFromReference(referencePath);
if (_schemasByName.TryGetValue(schemaName, out JObject referencedSchemaObject))
{
return referencedSchemaObject;
}
return schemaObject;
}
private string ResolveType(string propertyName, JObject propertySchemaObject)
{
if (propertySchemaObject["$ref"] != null)
{
string referencePath = propertySchemaObject.Value<string>("$ref");
string schemaName = GetSchemaNameFromReference(referencePath);
if (_schemasByName.TryGetValue(schemaName, out JObject referencedSchemaObject))
{
return BuildClassFromSchema(schemaName, referencedSchemaObject);
}
return NormalizeClassName(schemaName);
}
if (propertySchemaObject["allOf"] is JArray allOfArray)
{
foreach (JToken itemToken in allOfArray)
{
if (itemToken is JObject itemObject)
{
return ResolveType(propertyName, itemObject);
}
}
}
string typeName = propertySchemaObject.Value<string>("type");
string formatName = propertySchemaObject.Value<string>("format");
string resolvedTypeName;
switch (typeName)
{
case "integer":
resolvedTypeName = ResolveIntegerType(formatName);
break;
case "number":
resolvedTypeName = ResolveNumberType(formatName);
break;
case "boolean":
resolvedTypeName = "bool";
break;
case "string":
resolvedTypeName = ResolveStringType(formatName);
break;
case "array":
resolvedTypeName = ResolveArrayType(propertyName, propertySchemaObject);
break;
case "object":
resolvedTypeName = ResolveObjectType(propertyName, propertySchemaObject);
break;
default:
if (propertySchemaObject["enum"] is JArray)
{
resolvedTypeName = "string";
}
else if (propertySchemaObject["properties"] is JObject)
{
resolvedTypeName = BuildClassFromSchema(propertyName, propertySchemaObject);
}
else
{
resolvedTypeName = "object";
}
break;
}
bool isNullable = propertySchemaObject.Value<bool?>("nullable") == true;
if (isNullable && IsNullableValueType(resolvedTypeName))
{
return resolvedTypeName + "?";
}
return resolvedTypeName;
}
private string ResolveIntegerType(string formatName)
{
switch (formatName)
{
case "int64":
return "long";
case "int32":
default:
return "int";
}
}
private string ResolveNumberType(string formatName)
{
switch (formatName)
{
case "float":
return "float";
case "decimal":
return "decimal";
case "double":
default:
return "double";
}
}
private string ResolveStringType(string formatName)
{
switch (formatName)
{
case "date":
case "date-time":
return "DateTime";
case "uuid":
return "Guid";
case "byte":
case "binary":
return "byte[]";
default:
return "string";
}
}
private string ResolveArrayType(string propertyName, JObject propertySchemaObject)
{
if (propertySchemaObject["items"] is not JObject itemsObject)
{
return "List<object>";
}
string itemSuggestedName = ToSingularPascalCase(propertyName);
string itemTypeName = ResolveType(itemSuggestedName, itemsObject);
return $"List<{itemTypeName}>";
}
private string ResolveObjectType(string propertyName, JObject propertySchemaObject)
{
if (propertySchemaObject["properties"] is JObject)
{
return BuildClassFromSchema(propertyName, propertySchemaObject);
}
return "Dictionary<string, object>";
}
private string GetSchemaNameFromReference(string referencePath)
{
int lastSlashIndex = referencePath.LastIndexOf("/", StringComparison.Ordinal);
if (lastSlashIndex < 0)
{
return referencePath;
}
return referencePath.Substring(lastSlashIndex + 1);
}
private string BuildOutputCode()
{
StringBuilder builder = new StringBuilder();
builder.AppendLine("using System;");
builder.AppendLine("using System.Collections.Generic;");
builder.AppendLine();
foreach (ClassDefinition classDefinition in _classDefinitions)
{
builder.AppendLine("[Serializable]");
builder.AppendLine($"public class {classDefinition.Name}");
builder.AppendLine("{");
foreach (FieldDefinition field in classDefinition.Fields)
{
builder.AppendLine($" public {field.TypeName} {field.Name};");
}
builder.AppendLine("}");
builder.AppendLine();
}
return builder.ToString();
}
private string GetUniqueClassName(string suggestedClassName)
{
string className = suggestedClassName;
int index = 2;
while (_usedClassNames.Contains(className))
{
className = $"{suggestedClassName}{index}";
index++;
}
_usedClassNames.Add(className);
return className;
}
private string GetUniqueFieldName(string suggestedFieldName, HashSet<string> usedFieldNames)
{
string fieldName = NormalizeFieldName(suggestedFieldName);
string baseFieldName = fieldName;
int index = 2;
while (usedFieldNames.Contains(fieldName))
{
fieldName = $"{baseFieldName}{index}";
index++;
}
usedFieldNames.Add(fieldName);
return fieldName;
}
private string NormalizeClassName(string value)
{
string className = ToPascalCase(value);
if (IsCSharpKeyword(className))
{
return className + "Class";
}
return className;
}
private string NormalizeFieldName(string value)
{
string fieldName = ToPascalCase(value);
if (IsCSharpKeyword(fieldName))
{
return fieldName + "Value";
}
return fieldName;
}
private string ToSingularPascalCase(string value)
{
string pascalCase = ToPascalCase(value);
if (pascalCase.EndsWith("ies", StringComparison.OrdinalIgnoreCase) && pascalCase.Length > 3)
{
return pascalCase.Substring(0, pascalCase.Length - 3) + "y";
}
if (pascalCase.EndsWith("s", StringComparison.OrdinalIgnoreCase) &&
!pascalCase.EndsWith("ss", StringComparison.OrdinalIgnoreCase) &&
pascalCase.Length > 1)
{
return pascalCase.Substring(0, pascalCase.Length - 1);
}
return pascalCase + "Item";
}
private string ToPascalCase(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return "Value";
}
StringBuilder builder = new StringBuilder();
bool makeUpper = true;
foreach (char character in value)
{
if (!char.IsLetterOrDigit(character))
{
makeUpper = true;
continue;
}
if (builder.Length == 0 && char.IsDigit(character))
{
builder.Append("Value");
}
builder.Append(makeUpper
? char.ToUpperInvariant(character)
: character);
makeUpper = false;
}
return builder.Length == 0 ? "Value" : builder.ToString();
}
private bool IsNullableValueType(string typeName)
{
switch (typeName)
{
case "int":
case "long":
case "float":
case "double":
case "decimal":
case "bool":
case "DateTime":
case "Guid":
return true;
default:
return false;
}
}
private bool IsCSharpKeyword(string value)
{
switch (value)
{
case "abstract":
case "as":
case "base":
case "bool":
case "break":
case "byte":
case "case":
case "catch":
case "char":
case "checked":
case "class":
case "const":
case "continue":
case "decimal":
case "default":
case "delegate":
case "do":
case "double":
case "else":
case "enum":
case "event":
case "explicit":
case "extern":
case "false":
case "finally":
case "fixed":
case "float":
case "for":
case "foreach":
case "goto":
case "if":
case "implicit":
case "in":
case "int":
case "interface":
case "internal":
case "is":
case "lock":
case "long":
case "namespace":
case "new":
case "null":
case "object":
case "operator":
case "out":
case "override":
case "params":
case "private":
case "protected":
case "public":
case "readonly":
case "ref":
case "return":
case "sbyte":
case "sealed":
case "short":
case "sizeof":
case "stackalloc":
case "static":
case "string":
case "struct":
case "switch":
case "this":
case "throw":
case "true":
case "try":
case "typeof":
case "uint":
case "ulong":
case "unchecked":
case "unsafe":
case "ushort":
case "using":
case "virtual":
case "void":
case "volatile":
case "while":
return true;
default:
return false;
}
}
private sealed class ClassDefinition
{
public readonly string Name;
public readonly List<FieldDefinition> Fields = new List<FieldDefinition>();
public ClassDefinition(string name)
{
Name = name;
}
}
private sealed class FieldDefinition
{
public readonly string TypeName;
public readonly string Name;
public FieldDefinition(string typeName, string name)
{
TypeName = typeName;
Name = name;
}
}
}
}
#endif