From 69f8fc4d5e374a4d65885b602dffa53e65a502b0 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Mon, 30 Apr 2018 22:16:34 +0200 Subject: [PATCH] Added support for nested settings models so settings can be grouped together and reused for multiple providers. --- .../ClientSchemaTests/SchemaBuilderFixture.cs | 38 ++- src/NzbDrone.Api/ClientSchema/Field.cs | 7 +- src/NzbDrone.Api/ClientSchema/FieldMapping.cs | 15 ++ .../ClientSchema/SchemaBuilder.cs | 234 +++++++++++------- src/NzbDrone.Api/NzbDrone.Api.csproj | 1 + src/NzbDrone.Api/ProviderModuleBase.cs | 7 +- .../Extensions/TryParseExtensions.cs | 21 +- 7 files changed, 221 insertions(+), 102 deletions(-) create mode 100644 src/NzbDrone.Api/ClientSchema/FieldMapping.cs diff --git a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs index 385a9b989..bcd4bfdb5 100644 --- a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs +++ b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs @@ -21,19 +21,32 @@ namespace NzbDrone.Api.Test.ClientSchemaTests public void schema_should_have_proper_fields() { var model = new TestModel - { - FirstName = "Bob", - LastName = "Poop" - }; + { + FirstName = "Bob", + LastName = "Poop" + }; var schema = SchemaBuilder.ToSchema(model); - schema.Should().Contain(c => c.Order == 1 && c.Name == "LastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && (string) c.Value == "Poop"); - schema.Should().Contain(c => c.Order == 0 && c.Name == "FirstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && (string) c.Value == "Bob"); + schema.Should().Contain(c => c.Order == 1 && c.Name == "LastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && (string)c.Value == "Poop"); + schema.Should().Contain(c => c.Order == 0 && c.Name == "FirstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && (string)c.Value == "Bob"); } - } + [Test] + public void schema_should_have_nested_fields() + { + var model = new NestedTestModel(); + model.Name.FirstName = "Bob"; + model.Name.LastName = "Poop"; + + var schema = SchemaBuilder.ToSchema(model); + + schema.Should().Contain(c => c.Order == 0 && c.Name == "FirstName" && c.Label == "First Name" && c.HelpText == "Your First Name" && (string)c.Value == "Bob"); + schema.Should().Contain(c => c.Order == 1 && c.Name == "LastName" && c.Label == "Last Name" && c.HelpText == "Your Last Name" && (string)c.Value == "Poop"); + schema.Should().Contain(c => c.Order == 2 && c.Name == "Quote" && c.Label == "Quote" && c.HelpText == "Your Favorite Quote"); + } + } public class TestModel { @@ -45,4 +58,13 @@ namespace NzbDrone.Api.Test.ClientSchemaTests public string Other { get; set; } } -} \ No newline at end of file + + public class NestedTestModel + { + [FieldDefinition(0)] + public TestModel Name { get; set; } = new TestModel(); + + [FieldDefinition(1, Label = "Quote", HelpText = "Your Favorite Quote")] + public string Quote { get; set; } + } +} diff --git a/src/NzbDrone.Api/ClientSchema/Field.cs b/src/NzbDrone.Api/ClientSchema/Field.cs index ec611e8d6..358669e70 100644 --- a/src/NzbDrone.Api/ClientSchema/Field.cs +++ b/src/NzbDrone.Api/ClientSchema/Field.cs @@ -13,5 +13,10 @@ namespace NzbDrone.Api.ClientSchema public string Type { get; set; } public bool Advanced { get; set; } public List SelectOptions { get; set; } + + public Field Clone() + { + return (Field)MemberwiseClone(); + } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/ClientSchema/FieldMapping.cs b/src/NzbDrone.Api/ClientSchema/FieldMapping.cs new file mode 100644 index 000000000..93e90b792 --- /dev/null +++ b/src/NzbDrone.Api/ClientSchema/FieldMapping.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Api.ClientSchema +{ + public class FieldMapping + { + public Field Field { get; set; } + public Type PropertyType { get; set; } + public Func GetterFunc { get; set; } + public Action SetterFunc { get; set; } + } +} diff --git a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs b/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs index 0a7acb9e1..70f0ce1da 100644 --- a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs +++ b/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Newtonsoft.Json.Linq; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; @@ -11,24 +12,88 @@ namespace NzbDrone.Api.ClientSchema { public static class SchemaBuilder { + private static Dictionary _mappings = new Dictionary(); + public static List ToSchema(object model) { Ensure.That(model, () => model).IsNotNull(); - var properties = model.GetType().GetSimpleProperties(); + 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 result = new List(properties.Count); + var target = Activator.CreateInstance(targetType); - foreach (var propertyInfo in properties) + foreach (var mapping in mappings) { - var fieldAttribute = propertyInfo.GetAttribute(false); + var propertyType = mapping.PropertyType; + var field = fields.Find(f => f.Name == mapping.Field.Name); + + mapping.SetterFunc(target, field.Value); + } + + return target; + + } - if (fieldAttribute != null) + 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) + { + FieldMapping[] result; + if (!_mappings.TryGetValue(type, out 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 = propertyInfo.Name, + Name = prefix + propertyInfo.Name, Label = fieldAttribute.Label, HelpText = fieldAttribute.HelpText, HelpLink = fieldAttribute.HelpLink, @@ -37,120 +102,113 @@ namespace NzbDrone.Api.ClientSchema Type = fieldAttribute.Type.ToString().ToLowerInvariant() }; - var value = propertyInfo.GetValue(model, null); - if (value != null) - { - field.Value = value; - } - if (fieldAttribute.Type == FieldType.Select) { field.SelectOptions = GetSelectOptions(fieldAttribute.SelectOptions); } - result.Add(field); + 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, propertyInfo.Name + ".", t => propertyInfo.GetValue(targetSelector(t), null))); } } - return result.OrderBy(r => r.Order).ToList(); + return result.ToArray(); } - public static object ReadFromSchema(List fields, Type targetType) + private static Tuple[] GetProperties(Type type) { - Ensure.That(targetType, () => targetType).IsNotNull(); + return type.GetProperties() + .Select(v => Tuple.Create(v, v.GetAttribute(false))) + .Where(v => v.Item2 != null) + .OrderBy(v => v.Item2.Order) + .ToArray(); + } - var properties = targetType.GetSimpleProperties(); + private static List GetSelectOptions(Type selectOptions) + { + var options = from Enum e in Enum.GetValues(selectOptions) + select new SelectOption { Value = Convert.ToInt32(e), Name = e.ToString() }; - var target = Activator.CreateInstance(targetType); + return options.OrderBy(o => o.Value).ToList(); + } - foreach (var propertyInfo in properties) + private static Func GetValueConverter(Type propertyType) + { + if (propertyType == typeof(int)) { - var fieldAttribute = propertyInfo.GetAttribute(false); + return fieldValue => fieldValue?.ToString().ParseInt32() ?? 0; + } - if (fieldAttribute != null) - { - var field = fields.Find(f => f.Name == propertyInfo.Name); + else if (propertyType == typeof(long)) + { + return fieldValue => fieldValue?.ToString().ParseInt64() ?? 0; + } - if (propertyInfo.PropertyType == typeof(int)) - { - var value = field.Value.ToString().ParseInt32(); - propertyInfo.SetValue(target, value ?? 0, null); - } + else if (propertyType == typeof(double)) + { + return fieldValue => fieldValue?.ToString().ParseDouble() ?? 0.0; + } - else if (propertyInfo.PropertyType == typeof(long)) - { - var value = field.Value.ToString().ParseInt64(); - propertyInfo.SetValue(target, value ?? 0, null); - } + else if (propertyType == typeof(int?)) + { + return fieldValue => fieldValue?.ToString().ParseInt32(); + } - else if (propertyInfo.PropertyType == typeof(int?)) - { - var value = field.Value.ToString().ParseInt32(); - propertyInfo.SetValue(target, value, null); - } + else if (propertyType == typeof(Int64?)) + { + return fieldValue => fieldValue?.ToString().ParseInt64(); + } - else if (propertyInfo.PropertyType == typeof(Nullable)) + else if (propertyType == typeof(double?)) + { + return fieldValue => fieldValue?.ToString().ParseDouble(); + } + + else if (propertyType == typeof(IEnumerable)) + { + return fieldValue => + { + if (fieldValue.GetType() == typeof(JArray)) { - var value = field.Value.ToString().ParseInt64(); - propertyInfo.SetValue(target, value, null); + return ((JArray)fieldValue).Select(s => s.Value()); } - - else if (propertyInfo.PropertyType == typeof(IEnumerable)) + else { - IEnumerable value; - - if (field.Value.GetType() == typeof(JArray)) - { - value = ((JArray)field.Value).Select(s => s.Value()); - } - - else - { - value = field.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => Convert.ToInt32(s)); - } - - propertyInfo.SetValue(target, value, null); + return fieldValue.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => Convert.ToInt32(s)); } + }; + } - else if (propertyInfo.PropertyType == typeof(IEnumerable)) + else if (propertyType == typeof(IEnumerable)) + { + return fieldValue => + { + if (fieldValue.GetType() == typeof(JArray)) { - IEnumerable value; - - if (field.Value.GetType() == typeof(JArray)) - { - value = ((JArray)field.Value).Select(s => s.Value()); - } - - else - { - value = field.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - } - - propertyInfo.SetValue(target, value, null); + return ((JArray)fieldValue).Select(s => s.Value()); } - else { - propertyInfo.SetValue(target, field.Value, null); + return fieldValue.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); } - } + }; } - return target; - - } - - public static T ReadFromSchema(List fields) - { - return (T)ReadFromSchema(fields, typeof(T)); - } - - private static List GetSelectOptions(Type selectOptions) - { - var options = from Enum e in Enum.GetValues(selectOptions) - select new SelectOption { Value = Convert.ToInt32(e), Name = e.ToString() }; - - return options.OrderBy(o => o.Value).ToList(); + else + { + return fieldValue => fieldValue; + } } } } diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 16e830d4f..dfa4bfb4e 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -104,6 +104,7 @@ + diff --git a/src/NzbDrone.Api/ProviderModuleBase.cs b/src/NzbDrone.Api/ProviderModuleBase.cs index b45727227..d7ad2ec67 100644 --- a/src/NzbDrone.Api/ProviderModuleBase.cs +++ b/src/NzbDrone.Api/ProviderModuleBase.cs @@ -210,7 +210,12 @@ namespace NzbDrone.Api protected void VerifyValidationResult(ValidationResult validationResult, bool includeWarnings) { - var result = new NzbDroneValidationResult(validationResult.Errors); + var result = validationResult as NzbDroneValidationResult; + + if (result == null) + { + result = new NzbDroneValidationResult(validationResult.Errors); + } if (includeWarnings && (!result.IsValid || result.HasWarnings)) { diff --git a/src/NzbDrone.Common/Extensions/TryParseExtensions.cs b/src/NzbDrone.Common/Extensions/TryParseExtensions.cs index c485fbd54..fe504b97b 100644 --- a/src/NzbDrone.Common/Extensions/TryParseExtensions.cs +++ b/src/NzbDrone.Common/Extensions/TryParseExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; namespace NzbDrone.Common.Extensions { @@ -6,7 +7,7 @@ namespace NzbDrone.Common.Extensions { public static int? ParseInt32(this string source) { - int result = 0; + int result; if (int.TryParse(source, out result)) { @@ -16,9 +17,9 @@ namespace NzbDrone.Common.Extensions return null; } - public static Nullable ParseInt64(this string source) + public static long? ParseInt64(this string source) { - long result = 0; + long result; if (long.TryParse(source, out result)) { @@ -27,5 +28,17 @@ namespace NzbDrone.Common.Extensions return null; } + + public static double? ParseDouble(this string source) + { + double result; + + if (double.TryParse(source.Replace(',', '.'), NumberStyles.Number, CultureInfo.InvariantCulture, out result)) + { + return result; + } + + return null; + } } -} \ No newline at end of file +}