using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.Json; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Common.Reflection; using NzbDrone.Common.Serializer; using NzbDrone.Core.Annotations; namespace Lidarr.Http.ClientSchema { public static class SchemaBuilder { private static Dictionary _mappings = new Dictionary(); public static List ToSchema(object model) { Ensure.That(model, () => model).IsNotNull(); var mappings = GetFieldMappings(model.GetType()); var result = new List(mappings.Length); foreach (var mapping in mappings) { var field = mapping.Field.Clone(); field.Value = mapping.GetterFunc(model); result.Add(field); } return result.OrderBy(r => r.Order).ToList(); } public static object ReadFromSchema(List fields, Type targetType) { Ensure.That(targetType, () => targetType).IsNotNull(); var mappings = GetFieldMappings(targetType); var target = Activator.CreateInstance(targetType); foreach (var mapping in mappings) { var field = fields.Find(f => f.Name == mapping.Field.Name); if (field != null) { mapping.SetterFunc(target, field.Value); } } return target; } public static T ReadFromSchema(List fields) { return (T)ReadFromSchema(fields, typeof(T)); } // Ideally this function should begin a System.Linq.Expression expression tree since it's faster. // But it's probably not needed till performance issues pop up. public static FieldMapping[] GetFieldMappings(Type type) { lock (_mappings) { if (!_mappings.TryGetValue(type, out var result)) { result = GetFieldMapping(type, "", v => v); // Renumber al the field Orders since nested settings will have dupe Orders. for (int i = 0; i < result.Length; i++) { result[i].Field.Order = i; } _mappings[type] = result; } return result; } } private static FieldMapping[] GetFieldMapping(Type type, string prefix, Func targetSelector) { var result = new List(); foreach (var property in GetProperties(type)) { var propertyInfo = property.Item1; if (propertyInfo.PropertyType.IsSimpleType()) { var fieldAttribute = property.Item2; var field = new Field { Name = prefix + GetCamelCaseName(propertyInfo.Name), Label = fieldAttribute.Label, Unit = fieldAttribute.Unit, HelpText = fieldAttribute.HelpText, HelpLink = fieldAttribute.HelpLink, Order = fieldAttribute.Order, Advanced = fieldAttribute.Advanced, Type = fieldAttribute.Type.ToString().FirstCharToLower(), Section = fieldAttribute.Section, Placeholder = fieldAttribute.Placeholder }; if (fieldAttribute.Type == FieldType.Select || fieldAttribute.Type == FieldType.TagSelect) { if (fieldAttribute.SelectOptionsProviderAction.IsNotNullOrWhiteSpace()) { field.SelectOptionsProviderAction = fieldAttribute.SelectOptionsProviderAction; } else { field.SelectOptions = GetSelectOptions(fieldAttribute.SelectOptions); } } if (fieldAttribute.Hidden != HiddenType.Visible) { field.Hidden = fieldAttribute.Hidden.ToString().FirstCharToLower(); } var valueConverter = GetValueConverter(propertyInfo.PropertyType); result.Add(new FieldMapping { Field = field, PropertyType = propertyInfo.PropertyType, GetterFunc = t => propertyInfo.GetValue(targetSelector(t), null), SetterFunc = (t, v) => propertyInfo.SetValue(targetSelector(t), valueConverter(v), null) }); } else { result.AddRange(GetFieldMapping(propertyInfo.PropertyType, GetCamelCaseName(propertyInfo.Name) + ".", t => propertyInfo.GetValue(targetSelector(t), null))); } } return result.ToArray(); } private static Tuple[] GetProperties(Type type) { return type.GetProperties() .Select(v => Tuple.Create(v, v.GetAttribute(false))) .Where(v => v.Item2 != null) .OrderBy(v => v.Item2.Order) .ToArray(); } private static List GetSelectOptions(Type selectOptions) { var options = selectOptions.GetFields().Where(v => v.IsStatic).Select(v => { var name = v.Name.Replace('_', ' '); var value = Convert.ToInt32(v.GetRawConstantValue()); var attrib = v.GetCustomAttribute(); if (attrib != null) { return new SelectOption { Value = value, Name = attrib.Label ?? name, Order = attrib.Order, Hint = attrib.Hint ?? $"({value})" }; } else { return new SelectOption { Value = value, Name = name, Order = value }; } }); return options.OrderBy(o => o.Order).ToList(); } private static Func GetValueConverter(Type propertyType) { if (propertyType == typeof(int)) { return fieldValue => fieldValue?.ToString().ParseInt32() ?? 0; } else if (propertyType == typeof(long)) { return fieldValue => fieldValue?.ToString().ParseInt64() ?? 0; } else if (propertyType == typeof(double)) { return fieldValue => fieldValue?.ToString().ParseDouble() ?? 0.0; } else if (propertyType == typeof(int?)) { return fieldValue => fieldValue?.ToString().ParseInt32(); } else if (propertyType == typeof(long?)) { return fieldValue => fieldValue?.ToString().ParseInt64(); } else if (propertyType == typeof(double?)) { return fieldValue => fieldValue?.ToString().ParseDouble(); } else if (propertyType == typeof(IEnumerable)) { return fieldValue => { if (fieldValue == null) { return Enumerable.Empty(); } else if (fieldValue is JsonElement e && e.ValueKind == JsonValueKind.Array) { return e.EnumerateArray().Select(s => s.GetInt32()); } else { return fieldValue.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => Convert.ToInt32(s)); } }; } else if (propertyType == typeof(IEnumerable)) { return fieldValue => { if (fieldValue == null) { return Enumerable.Empty(); } else if (fieldValue is JsonElement e && e.ValueKind == JsonValueKind.Array) { return e.EnumerateArray().Select(s => s.GetString()); } else { return fieldValue.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); } }; } else { return fieldValue => { var element = fieldValue as JsonElement?; if (element == null || !element.HasValue) { return null; } var json = element.Value.GetRawText(); return STJson.Deserialize(json, propertyType); }; } } private static string GetCamelCaseName(string name) { return char.ToLowerInvariant(name[0]) + name.Substring(1); } } }