using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.Json; using DryIoc; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Common.Reflection; using NzbDrone.Common.Serializer; using NzbDrone.Core.Annotations; using NzbDrone.Core.Localization; namespace Readarr.Http.ClientSchema { public static class SchemaBuilder { private static Dictionary _mappings = new Dictionary(); private static ILocalizationService _localizationService; public static void Initialize(IContainer container) { _localizationService = container.Resolve(); } 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 (var 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 label = fieldAttribute.Label.IsNotNullOrWhiteSpace() ? _localizationService.GetLocalizedString(fieldAttribute.Label, GetTokens(type, fieldAttribute.Label, TokenField.Label)) : fieldAttribute.Label; var helpText = fieldAttribute.HelpText.IsNotNullOrWhiteSpace() ? _localizationService.GetLocalizedString(fieldAttribute.HelpText, GetTokens(type, fieldAttribute.Label, TokenField.HelpText)) : fieldAttribute.HelpText; var helpTextWarning = fieldAttribute.HelpTextWarning.IsNotNullOrWhiteSpace() ? _localizationService.GetLocalizedString(fieldAttribute.HelpTextWarning, GetTokens(type, fieldAttribute.Label, TokenField.HelpTextWarning)) : fieldAttribute.HelpTextWarning; var field = new Field { Name = prefix + GetCamelCaseName(propertyInfo.Name), Label = label, Unit = fieldAttribute.Unit, HelpText = helpText, HelpTextWarning = helpTextWarning, HelpLink = fieldAttribute.HelpLink, Order = fieldAttribute.Order, Advanced = fieldAttribute.Advanced, Type = fieldAttribute.Type.ToString().FirstCharToLower(), Section = fieldAttribute.Section, Placeholder = fieldAttribute.Placeholder }; if (fieldAttribute.Type is FieldType.Select or 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(); } if (fieldAttribute.Type is FieldType.Number && propertyInfo.PropertyType == typeof(double)) { field.IsFloat = true; } 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 Dictionary GetTokens(Type type, string label, TokenField field) { var tokens = new Dictionary(); foreach (var propertyInfo in type.GetProperties()) { foreach (var attribute in propertyInfo.GetCustomAttributes(true)) { if (attribute is FieldTokenAttribute fieldTokenAttribute && fieldTokenAttribute.Field == field && fieldTokenAttribute.Label == label) { tokens.Add(fieldTokenAttribute.Token, fieldTokenAttribute.Value); } } } return tokens; } private static List GetSelectOptions(Type selectOptions) { if (selectOptions.IsEnum) { var options = selectOptions .GetFields() .Where(v => v.IsStatic && !v.GetCustomAttributes(false).OfType().Any()) .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})" }; } return new SelectOption { Value = value, Name = name, Order = value }; }); return options.OrderBy(o => o.Order).ToList(); } throw new NotSupportedException(); } 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).Select(v => v.Trim()); } }; } 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); } } }