Загрузка данных


private string BuildOutputCode()
{
    StringBuilder builder = new StringBuilder();

    builder.AppendLine("using System;");
    builder.AppendLine("using System.Collections.Generic;");
    builder.AppendLine();

    if (!string.IsNullOrWhiteSpace(_rootComment))
    {
        builder.AppendLine(_rootComment);
        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("}");
        builder.AppendLine();
    }

    return builder.ToString();
}

#if UNITY_EDITOR

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;

namespace CodeBase.EditorTools
{
    public sealed class JsonToCSharpConverterWindow : EditorWindow
    {
        private const string RootClassName = "Root";

        private string _inputJson = string.Empty;
        private string _outputCode = string.Empty;

        private Vector2 _inputScrollPosition;
        private Vector2 _outputScrollPosition;

        [MenuItem("Tools/JSON To C# Class Converter")]
        public static void Open()
        {
            JsonToCSharpConverterWindow window = GetWindow<JsonToCSharpConverterWindow>("JSON To C#");
            window.minSize = new Vector2(700f, 500f);
            window.Show();
        }

        private void OnGUI()
        {
            EditorGUILayout.LabelField("JSON", EditorStyles.boldLabel);

            _inputScrollPosition = EditorGUILayout.BeginScrollView(
                _inputScrollPosition,
                GUILayout.MinHeight(180f),
                GUILayout.ExpandHeight(true));

            _inputJson = EditorGUILayout.TextArea(_inputJson, GUILayout.ExpandHeight(true));

            EditorGUILayout.EndScrollView();

            EditorGUILayout.Space(8f);

            if (GUILayout.Button("Convert", GUILayout.Height(32f)))
            {
                ConvertJsonToCSharpClass();
            }

            EditorGUILayout.Space(8f);

            EditorGUILayout.LabelField("C# Class", EditorStyles.boldLabel);

            _outputScrollPosition = EditorGUILayout.BeginScrollView(
                _outputScrollPosition,
                GUILayout.MinHeight(220f),
                GUILayout.ExpandHeight(true));

            _outputCode = EditorGUILayout.TextArea(_outputCode, GUILayout.ExpandHeight(true));

            EditorGUILayout.EndScrollView();
        }

        private void ConvertJsonToCSharpClass()
        {
            if (string.IsNullOrWhiteSpace(_inputJson))
            {
                _outputCode = "// JSON is empty.";
                return;
            }

            try
            {
                JToken rootToken = JToken.Parse(_inputJson);

                JsonCSharpClassGenerator generator = new JsonCSharpClassGenerator();
                _outputCode = generator.Generate(rootToken, RootClassName);
            }
            catch (Exception exception)
            {
                _outputCode = $"// Convert failed:\n// {exception.Message}";
            }
        }
    }

    internal sealed class JsonCSharpClassGenerator
    {
        private readonly List<ClassDefinition> _classDefinitions = new List<ClassDefinition>();
        private readonly HashSet<string> _usedClassNames = new HashSet<string>();

        private string _rootComment = string.Empty;

        public string Generate(JToken rootToken, string rootClassName)
        {
            _classDefinitions.Clear();
            _usedClassNames.Clear();
            _rootComment = string.Empty;

            if (rootToken.Type == JTokenType.Object)
            {
                BuildClassFromObjects(new List<JObject> { (JObject)rootToken }, rootClassName);
            }
            else if (rootToken.Type == JTokenType.Array)
            {
                BuildRootArrayClass((JArray)rootToken, rootClassName);
            }
            else
            {
                BuildPrimitiveRootClass(rootToken, rootClassName);
            }

            return BuildOutputCode();
        }

        private void BuildRootArrayClass(JArray rootArray, string rootClassName)
        {
            List<JToken> notNullItems = rootArray
                .Where(token => token.Type != JTokenType.Null && token.Type != JTokenType.Undefined)
                .ToList();

            if (notNullItems.Count == 0)
            {
                BuildWrapperClass(rootClassName, "List<object>", "Items", "items");
                return;
            }

            bool containsOnlyObjects = notNullItems.All(token => token.Type == JTokenType.Object);

            if (containsOnlyObjects)
            {
                List<JObject> objects = notNullItems
                    .Cast<JObject>()
                    .ToList();

                BuildClassFromObjects(objects, rootClassName);
                _rootComment = $"// Root JSON is an array. Deserialize it as List<{NormalizeClassName(rootClassName)}>.";
                return;
            }

            string itemTypeName = InferType(notNullItems, "Item");
            BuildWrapperClass(rootClassName, $"List<{itemTypeName}>", "Items", "items");
        }

        private void BuildPrimitiveRootClass(JToken rootToken, string rootClassName)
        {
            string valueTypeName = GetPrimitiveTypeName(rootToken);
            BuildWrapperClass(rootClassName, valueTypeName, "Value", "value");
        }

        private void BuildWrapperClass(
            string suggestedClassName,
            string valueTypeName,
            string fieldName,
            string jsonName)
        {
            string className = GetUniqueClassName(suggestedClassName);

            ClassDefinition classDefinition = new ClassDefinition(className);
            classDefinition.Fields.Add(new FieldDefinition(valueTypeName, fieldName, jsonName));

            _classDefinitions.Add(classDefinition);
        }

        private string BuildClassFromObjects(List<JObject> objects, string suggestedClassName)
        {
            string className = GetUniqueClassName(suggestedClassName);

            ClassDefinition classDefinition = new ClassDefinition(className);
            _classDefinitions.Add(classDefinition);

            List<string> jsonPropertyNames = objects
                .SelectMany(jsonObject => jsonObject.Properties())
                .Select(property => property.Name)
                .Distinct()
                .ToList();

            HashSet<string> usedFieldNames = new HashSet<string>();

            foreach (string jsonPropertyName in jsonPropertyNames)
            {
                List<JToken> propertyValues = objects
                    .Select(jsonObject => jsonObject[jsonPropertyName])
                    .Where(value => value != null)
                    .ToList();

                string fieldTypeName = InferType(propertyValues, jsonPropertyName);
                string fieldName = GetUniqueFieldName(ToPascalCase(jsonPropertyName), usedFieldNames);

                classDefinition.Fields.Add(new FieldDefinition(fieldTypeName, fieldName, jsonPropertyName));
            }

            return className;
        }

        private string InferType(List<JToken> tokens, string suggestedName)
        {
            List<JToken> notNullTokens = tokens
                .Where(token => token.Type != JTokenType.Null && token.Type != JTokenType.Undefined)
                .ToList();

            if (notNullTokens.Count == 0)
            {
                return "object";
            }

            bool containsOnlyObjects = notNullTokens.All(token => token.Type == JTokenType.Object);

            if (containsOnlyObjects)
            {
                List<JObject> objects = notNullTokens
                    .Cast<JObject>()
                    .ToList();

                return BuildClassFromObjects(objects, suggestedName);
            }

            bool containsOnlyArrays = notNullTokens.All(token => token.Type == JTokenType.Array);

            if (containsOnlyArrays)
            {
                List<JArray> arrays = notNullTokens
                    .Cast<JArray>()
                    .ToList();

                return InferArrayType(arrays, suggestedName);
            }

            bool containsComplexToken = notNullTokens.Any(token =>
                token.Type == JTokenType.Object ||
                token.Type == JTokenType.Array);

            if (containsComplexToken)
            {
                return "object";
            }

            List<string> primitiveTypes = notNullTokens
                .Select(GetPrimitiveTypeName)
                .Distinct()
                .ToList();

            if (primitiveTypes.Count == 1)
            {
                return primitiveTypes[0];
            }

            bool containsOnlyNumbers = primitiveTypes.All(typeName =>
                typeName == "int" ||
                typeName == "long" ||
                typeName == "float" ||
                typeName == "double" ||
                typeName == "decimal");

            return containsOnlyNumbers ? "double" : "object";
        }

        private string InferArrayType(List<JArray> arrays, string suggestedName)
        {
            List<JToken> items = arrays
                .SelectMany(array => array)
                .Where(token => token.Type != JTokenType.Null && token.Type != JTokenType.Undefined)
                .ToList();

            if (items.Count == 0)
            {
                return "List<object>";
            }

            string itemClassName = ToSingularPascalCase(suggestedName);
            string itemTypeName = InferType(items, itemClassName);

            return $"List<{itemTypeName}>";
        }

        private string GetPrimitiveTypeName(JToken token)
        {
            switch (token.Type)
            {
                case JTokenType.Integer:
                    long integerValue = token.Value<long>();
                    return integerValue >= int.MinValue && integerValue <= int.MaxValue
                        ? "int"
                        : "long";

                case JTokenType.Float:
                    return "double";

                case JTokenType.Boolean:
                    return "bool";

                case JTokenType.String:
                    return "string";

                case JTokenType.Date:
                    return "DateTime";

                case JTokenType.Guid:
                    return "Guid";

                case JTokenType.TimeSpan:
                    return "TimeSpan";

                default:
                    return "object";
            }
        }

        private string BuildOutputCode()
        {
            StringBuilder builder = new StringBuilder();

            builder.AppendLine("using System;");
            builder.AppendLine("using System.Collections.Generic;");
            builder.AppendLine("using Newtonsoft.Json;");
            builder.AppendLine();

            if (!string.IsNullOrWhiteSpace(_rootComment))
            {
                builder.AppendLine(_rootComment);
                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($"    [JsonProperty(\"{EscapeJsonPropertyName(field.JsonName)}\")]");
                    builder.AppendLine($"    public {field.TypeName} {field.Name};");
                    builder.AppendLine();
                }

                builder.AppendLine("}");
                builder.AppendLine();
            }

            return builder.ToString();
        }

        private string GetUniqueClassName(string suggestedClassName)
        {
            string baseClassName = NormalizeClassName(suggestedClassName);
            string className = baseClassName;

            int index = 2;

            while (_usedClassNames.Contains(className))
            {
                className = $"{baseClassName}{index}";
                index++;
            }

            _usedClassNames.Add(className);

            return className;
        }

        private string GetUniqueFieldName(string suggestedFieldName, HashSet<string> usedFieldNames)
        {
            string baseFieldName = NormalizeFieldName(suggestedFieldName);
            string fieldName = baseFieldName;

            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))
            {
                className += "Class";
            }

            return className;
        }

        private string NormalizeFieldName(string value)
        {
            string fieldName = ToPascalCase(value);

            if (IsCSharpKeyword(fieldName))
            {
                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 string EscapeJsonPropertyName(string value)
        {
            return value
                .Replace("\\", "\\\\")
                .Replace("\"", "\\\"");
        }

        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 readonly string JsonName;

            public FieldDefinition(string typeName, string name, string jsonName)
            {
                TypeName = typeName;
                Name = name;
                JsonName = jsonName;
            }
        }
    }
}

#endif