Added support for nested settings models so settings can be grouped together and reused for multiple providers.

pull/2577/head
Taloth Saldono 6 years ago
parent b339fcbd82
commit 69f8fc4d5e

@ -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; }
}
}
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; }
}
}

@ -13,5 +13,10 @@ namespace NzbDrone.Api.ClientSchema
public string Type { get; set; }
public bool Advanced { get; set; }
public List<SelectOption> SelectOptions { get; set; }
public Field Clone()
{
return (Field)MemberwiseClone();
}
}
}
}

@ -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<object, object> GetterFunc { get; set; }
public Action<object, object> SetterFunc { get; set; }
}
}

@ -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<Type, FieldMapping[]> _mappings = new Dictionary<Type, FieldMapping[]>();
public static List<Field> ToSchema(object model)
{
Ensure.That(model, () => model).IsNotNull();
var properties = model.GetType().GetSimpleProperties();
var mappings = GetFieldMappings(model.GetType());
var result = new List<Field>(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<Field> fields, Type targetType)
{
Ensure.That(targetType, () => targetType).IsNotNull();
var mappings = GetFieldMappings(targetType);
var result = new List<Field>(properties.Count);
var target = Activator.CreateInstance(targetType);
foreach (var propertyInfo in properties)
foreach (var mapping in mappings)
{
var fieldAttribute = propertyInfo.GetAttribute<FieldDefinitionAttribute>(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<T>(List<Field> 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<object, object> targetSelector)
{
var result = new List<FieldMapping>();
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<Field> fields, Type targetType)
private static Tuple<PropertyInfo, FieldDefinitionAttribute>[] GetProperties(Type type)
{
Ensure.That(targetType, () => targetType).IsNotNull();
return type.GetProperties()
.Select(v => Tuple.Create(v, v.GetAttribute<FieldDefinitionAttribute>(false)))
.Where(v => v.Item2 != null)
.OrderBy(v => v.Item2.Order)
.ToArray();
}
var properties = targetType.GetSimpleProperties();
private static List<SelectOption> 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<object, object> GetValueConverter(Type propertyType)
{
if (propertyType == typeof(int))
{
var fieldAttribute = propertyInfo.GetAttribute<FieldDefinitionAttribute>(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<Int64>))
else if (propertyType == typeof(double?))
{
return fieldValue => fieldValue?.ToString().ParseDouble();
}
else if (propertyType == typeof(IEnumerable<int>))
{
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<int>());
}
else if (propertyInfo.PropertyType == typeof(IEnumerable<int>))
else
{
IEnumerable<int> value;
if (field.Value.GetType() == typeof(JArray))
{
value = ((JArray)field.Value).Select(s => s.Value<int>());
}
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<string>))
else if (propertyType == typeof(IEnumerable<string>))
{
return fieldValue =>
{
if (fieldValue.GetType() == typeof(JArray))
{
IEnumerable<string> value;
if (field.Value.GetType() == typeof(JArray))
{
value = ((JArray)field.Value).Select(s => s.Value<string>());
}
else
{
value = field.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}
propertyInfo.SetValue(target, value, null);
return ((JArray)fieldValue).Select(s => s.Value<string>());
}
else
{
propertyInfo.SetValue(target, field.Value, null);
return fieldValue.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}
}
};
}
return target;
}
public static T ReadFromSchema<T>(List<Field> fields)
{
return (T)ReadFromSchema(fields, typeof(T));
}
private static List<SelectOption> 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;
}
}
}
}

@ -104,6 +104,7 @@
<Compile Include="Calendar\CalendarModule.cs" />
<Compile Include="ClientSchema\Field.cs" />
<Compile Include="ClientSchema\FieldDefinitionAttribute.cs" />
<Compile Include="ClientSchema\FieldMapping.cs" />
<Compile Include="ClientSchema\SchemaBuilder.cs" />
<Compile Include="ClientSchema\SchemaDeserializer.cs" />
<Compile Include="ClientSchema\SelectOption.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))
{

@ -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<long> 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;
}
}
}
}

Loading…
Cancel
Save