364 lines
12 KiB
C#
364 lines
12 KiB
C#
// Copyright © 2010 Xamasoft
|
|
|
|
using Humanizer;
|
|
using Json2CSharpCodeGenerator.Lib.CodeWriters;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using System.Text;
|
|
|
|
namespace Json2CSharpCodeGenerator.Lib;
|
|
|
|
public class JsonClassGenerator : IJsonClassGeneratorConfig
|
|
{
|
|
#region IJsonClassGeneratorConfig
|
|
|
|
public OutputTypes OutputType { get; set; } = OutputTypes.MutableClass;
|
|
public OutputCollectionType CollectionType { get; set; } = OutputCollectionType.MutableList;
|
|
public MutableClassConfig MutableClasses { get; } = new MutableClassConfig();
|
|
public JsonLibrary AttributeLibrary { get; set; } = JsonLibrary.NewtonsoftJson;
|
|
public JsonPropertyAttributeUsage AttributeUsage { get; set; } = JsonPropertyAttributeUsage.OnlyWhenNecessary;
|
|
|
|
public string Namespace { get; set; }
|
|
public string SecondaryNamespace { get; set; }
|
|
|
|
public bool InternalVisibility { get; set; }
|
|
public bool NoHelperClass { get; set; }
|
|
public string MainClass { get; set; }
|
|
public bool UsePascalCase { get; set; }
|
|
public bool UseNestedClasses { get; set; }
|
|
public bool ApplyObfuscationAttributes { get; set; }
|
|
public bool SingleFile { get; set; }
|
|
public ICodeBuilder CodeWriter { get; set; }
|
|
public bool AlwaysUseNullableValues { get; set; }
|
|
public bool ExamplesInDocumentation { get; set; }
|
|
public bool RemoveToJson { get; set; }
|
|
public bool RemoveFromJson { get; set; }
|
|
public bool RemoveConstructors { get; set; }
|
|
|
|
#endregion
|
|
|
|
public TextWriter OutputStream { get; set; }
|
|
|
|
//private readonly PluralizationService pluralizationService = PluralizationService.CreateService(new CultureInfo("en-US"));
|
|
|
|
public StringBuilder GenerateClasses(string jsonInput, out string errorMessage)
|
|
{
|
|
JObject[] examples = null;
|
|
bool rootWasArray = false;
|
|
try
|
|
{
|
|
using StringReader sr = new(jsonInput);
|
|
using JsonTextReader reader = new(sr);
|
|
JToken json = JToken.ReadFrom(reader);
|
|
if (json is JArray jArray && (jArray.Count == 0 || jArray.All(el => el is JObject)))
|
|
{
|
|
rootWasArray = true;
|
|
examples = jArray.Cast<JObject>().ToArray();
|
|
}
|
|
else if (json is JObject jObject)
|
|
{
|
|
examples = new[] { jObject };
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errorMessage = "Exception: " + ex.Message;
|
|
return new StringBuilder();
|
|
}
|
|
|
|
try
|
|
{
|
|
if (CodeWriter == null)
|
|
CodeWriter = new CSharpCodeWriter();
|
|
|
|
Types = new List<JsonType>();
|
|
_ = this._Names.Add("Root");
|
|
JsonType rootType = new(this, examples[0])
|
|
{
|
|
IsRoot = true
|
|
};
|
|
rootType.AssignName("Root");
|
|
GenerateClass(examples, rootType);
|
|
|
|
Types = HandleDuplicateClasses(Types);
|
|
|
|
StringBuilder builder = new();
|
|
WriteClassesToFile(builder, Types, rootWasArray);
|
|
|
|
errorMessage = string.Empty;
|
|
return builder;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errorMessage = ex.ToString();
|
|
return new StringBuilder();
|
|
}
|
|
}
|
|
|
|
private void WriteClassesToFile(StringBuilder sw, IEnumerable<JsonType> types, bool rootIsArray = false)
|
|
{
|
|
bool inNamespace = false;
|
|
bool rootNamespace = false;
|
|
|
|
CodeWriter.WriteFileStart(this, sw);
|
|
CodeWriter.WriteDeserializationComment(this, sw, rootIsArray);
|
|
|
|
foreach (JsonType type in types)
|
|
{
|
|
if (this.HasNamespace() && inNamespace && rootNamespace != type.IsRoot && SecondaryNamespace != null)
|
|
{
|
|
CodeWriter.WriteNamespaceEnd(this, sw, rootNamespace);
|
|
inNamespace = false;
|
|
}
|
|
|
|
if (this.HasNamespace() && !inNamespace)
|
|
{
|
|
CodeWriter.WriteNamespaceStart(this, sw, type.IsRoot);
|
|
inNamespace = true;
|
|
rootNamespace = type.IsRoot;
|
|
}
|
|
|
|
CodeWriter.WriteClass(this, sw, type);
|
|
}
|
|
|
|
if (this.HasNamespace() && inNamespace)
|
|
{
|
|
CodeWriter.WriteNamespaceEnd(this, sw, rootNamespace);
|
|
}
|
|
|
|
CodeWriter.WriteFileEnd(this, sw);
|
|
}
|
|
|
|
private void GenerateClass(JObject[] examples, JsonType type)
|
|
{
|
|
Dictionary<string, JsonType> jsonFields = new();
|
|
Dictionary<string, List<object>> fieldExamples = new();
|
|
|
|
bool first = true;
|
|
|
|
foreach (JObject obj in examples)
|
|
{
|
|
foreach (JProperty prop in obj.Properties())
|
|
{
|
|
JsonType fieldType;
|
|
JsonType currentType = new(this, prop.Value);
|
|
string propName = prop.Name;
|
|
|
|
if (jsonFields.TryGetValue(propName, out fieldType))
|
|
{
|
|
JsonType commonType = fieldType.GetCommonType(currentType);
|
|
|
|
jsonFields[propName] = commonType;
|
|
}
|
|
else
|
|
{
|
|
JsonType commonType = currentType;
|
|
if (first)
|
|
commonType = commonType.MaybeMakeNullable(this);
|
|
else
|
|
commonType = commonType.GetCommonType(JsonType.GetNull(this));
|
|
|
|
jsonFields.Add(propName, commonType);
|
|
fieldExamples[propName] = new List<object>();
|
|
}
|
|
|
|
List<object> fe = fieldExamples[propName];
|
|
JToken val = prop.Value;
|
|
if (val.Type is JTokenType.Null or JTokenType.Undefined)
|
|
{
|
|
if (!fe.Contains(null))
|
|
{
|
|
fe.Insert(0, null);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
object v = val.Type is JTokenType.Array or JTokenType.Object ? val : val.Value<object>();
|
|
if (!fe.Any(x => v.Equals(x)))
|
|
{
|
|
fe.Add(v);
|
|
}
|
|
}
|
|
}
|
|
first = false;
|
|
}
|
|
|
|
if (UseNestedClasses)
|
|
{
|
|
foreach (KeyValuePair<string, JsonType> field in jsonFields)
|
|
{
|
|
_ = this._Names.Add(field.Key.ToLower());
|
|
}
|
|
}
|
|
|
|
foreach (KeyValuePair<string, JsonType> field in jsonFields)
|
|
{
|
|
JsonType fieldType = field.Value;
|
|
if (fieldType.Type == JsonTypeEnum.Object)
|
|
{
|
|
List<JObject> subexamples = new(examples.Length);
|
|
foreach (JObject obj in examples)
|
|
{
|
|
JToken value;
|
|
if (obj.TryGetValue(field.Key, out value))
|
|
{
|
|
if (value.Type == JTokenType.Object)
|
|
{
|
|
subexamples.Add((JObject)value);
|
|
}
|
|
}
|
|
}
|
|
|
|
fieldType.AssignOriginalName(field.Key);
|
|
fieldType.AssignName(CreateUniqueClassName(field.Key));
|
|
fieldType.AssignNewAssignedName(ToTitleCase(field.Key));
|
|
|
|
GenerateClass(subexamples.ToArray(), fieldType);
|
|
}
|
|
|
|
if (fieldType.InternalType != null && fieldType.InternalType.Type == JsonTypeEnum.Object)
|
|
{
|
|
List<JObject> subexamples = new(examples.Length);
|
|
foreach (JObject obj in examples)
|
|
{
|
|
JToken value;
|
|
if (obj.TryGetValue(field.Key, out value))
|
|
{
|
|
if (value is JArray jArray)
|
|
{
|
|
const int MAX_JSON_ARRAY_ITEMS = 50; // Take like 30 items from the array this will increase the chance of getting all the objects accuralty while not analyzing all the data
|
|
|
|
subexamples.AddRange(jArray.OfType<JObject>().Take(MAX_JSON_ARRAY_ITEMS));
|
|
}
|
|
else if (value is JObject jObject) //TODO J2C : ONLY LOOP OVER 50 OBJECT AND NOT THE WHOLE THING
|
|
{
|
|
foreach (KeyValuePair<string, JToken> jsonObjectProperty in jObject)
|
|
{
|
|
// if (!(item.Value is JObject)) throw new NotSupportedException("Arrays of non-objects are not supported yet.");
|
|
if (jsonObjectProperty.Value is JObject innerObject)
|
|
{
|
|
subexamples.Add(innerObject);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
field.Value.InternalType.AssignOriginalName(field.Key);
|
|
field.Value.InternalType.AssignName(CreateUniqueClassNameFromPlural(field.Key));
|
|
field.Value.InternalType.AssignNewAssignedName(ToTitleCase(field.Key).Singularize(inputIsKnownToBePlural: false));
|
|
|
|
GenerateClass(subexamples.ToArray(), field.Value.InternalType);
|
|
}
|
|
}
|
|
|
|
type.Fields = jsonFields
|
|
.Select(x => new FieldInfo(
|
|
generator: this,
|
|
jsonMemberName: x.Key,
|
|
type: x.Value,
|
|
usePascalCase: UsePascalCase || AttributeUsage == JsonPropertyAttributeUsage.Always,
|
|
examples: fieldExamples[x.Key])
|
|
)
|
|
.ToList();
|
|
|
|
if (!string.IsNullOrEmpty(type.AssignedName))
|
|
{
|
|
Types.Add(type);
|
|
}
|
|
}
|
|
|
|
/// <summary>Checks if there are any duplicate classes in the input, and merges its corresponding properties (TEST CASE 7)</summary>
|
|
private IList<JsonType> HandleDuplicateClasses(IList<JsonType> types)
|
|
{
|
|
// TODO: This is currently O(n*n) because it iterates through List<T> on every loop iteration. This can be optimized.
|
|
|
|
List<JsonType> typesWithNoDuplicates = new();
|
|
types = types.OrderBy(p => p.AssignedName).ToList();
|
|
foreach (JsonType type in types)
|
|
{
|
|
if (!typesWithNoDuplicates.Exists(p => p.OriginalName == type.OriginalName))
|
|
{
|
|
typesWithNoDuplicates.Add(type);
|
|
}
|
|
else
|
|
{
|
|
JsonType duplicatedType = typesWithNoDuplicates.FirstOrDefault(p => p.OriginalName == type.OriginalName);
|
|
|
|
// Rename all references of this type to the original assigned name
|
|
foreach (FieldInfo field in type.Fields)
|
|
{
|
|
if (!duplicatedType.Fields.ToList().Exists(x => x.JsonMemberName == field.JsonMemberName))
|
|
{
|
|
duplicatedType.Fields.Add(field);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return typesWithNoDuplicates;
|
|
}
|
|
|
|
public IList<JsonType> Types { get; private set; }
|
|
private readonly HashSet<string> _Names = new();
|
|
|
|
private string CreateUniqueClassName(string name)
|
|
{
|
|
name = ToTitleCase(name);
|
|
|
|
string finalName = name;
|
|
int i = 2;
|
|
while (this._Names.Any(x => x.Equals(finalName, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
|
|
finalName = name + i.ToString();
|
|
i++;
|
|
}
|
|
|
|
_ = this._Names.Add(finalName);
|
|
return finalName;
|
|
}
|
|
|
|
private string CreateUniqueClassNameFromPlural(string plural)
|
|
{
|
|
plural = ToTitleCase(plural);
|
|
string singular = plural.Singularize(inputIsKnownToBePlural: false);
|
|
return CreateUniqueClassName(singular);
|
|
}
|
|
|
|
internal static string ToTitleCase(string str)
|
|
{
|
|
StringBuilder sb = new(str.Length);
|
|
bool flag = true;
|
|
|
|
for (int i = 0; i < str.Length; i++)
|
|
{
|
|
char c = str[i];
|
|
string specialCaseFirstCharIsNumber = string.Empty;
|
|
|
|
// Handle the case where the first character is a number
|
|
if (i == 0 && char.IsDigit(c))
|
|
specialCaseFirstCharIsNumber = "_" + c;
|
|
|
|
if (char.IsLetterOrDigit(c))
|
|
{
|
|
if (string.IsNullOrEmpty(specialCaseFirstCharIsNumber))
|
|
{
|
|
_ = sb.Append(flag ? char.ToUpper(c) : c);
|
|
}
|
|
else
|
|
{
|
|
_ = sb.Append(flag ? specialCaseFirstCharIsNumber.ToUpper() : specialCaseFirstCharIsNumber);
|
|
}
|
|
|
|
flag = false;
|
|
}
|
|
else
|
|
{
|
|
flag = true;
|
|
}
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
} |