diff --git a/NzbDrone.Api.Test/MappingTests/ReflectionExtensionFixture.cs b/NzbDrone.Api.Test/MappingTests/ReflectionExtensionFixture.cs new file mode 100644 index 000000000..6d0469b12 --- /dev/null +++ b/NzbDrone.Api.Test/MappingTests/ReflectionExtensionFixture.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Reflection; +using NzbDrone.Core.Datastore; +using NzbDrone.Test.Common; + +namespace NzbDrone.Api.Test.MappingTests +{ + public class ReflectionExtensionFixture : TestBase + { + [Test] + public void should_get_properties_from_models() + { + var models = Assembly.Load("NzbDrone.Core").ImplementationsOf(); + + foreach (var model in models) + { + model.GetSimpleProperties().Should().NotBeEmpty(); + } + } + + [Test] + public void should_be_able_to_get_implementations() + { + var models = Assembly.Load("NzbDrone.Core").ImplementationsOf(); + + models.Should().NotBeEmpty(); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs b/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs new file mode 100644 index 000000000..61c85d643 --- /dev/null +++ b/NzbDrone.Api.Test/MappingTests/ResourceMappingFixture.cs @@ -0,0 +1,21 @@ +using System; +using NUnit.Framework; +using NzbDrone.Api.Episodes; +using NzbDrone.Api.Mapping; +using NzbDrone.Api.Series; +using NzbDrone.Test.Common; + +namespace NzbDrone.Api.Test.MappingTests +{ + [TestFixture] + public class ResourceMappingFixture : TestBase + { + [TestCase(typeof(Core.Tv.Series), typeof(SeriesResource))] + [TestCase(typeof(Core.Tv.Episode), typeof(EpisodeResource))] + public void matching_fields(Type modelType, Type resourceType) + { + MappingValidation.ValidateMapping(modelType, resourceType); + } + + } +} \ No newline at end of file diff --git a/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj b/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj index 6298e65ee..0c884e4f9 100644 --- a/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj +++ b/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj @@ -50,9 +50,19 @@ MinimumRecommendedRules.ruleset + + ..\packages\NBuilder.3.0.1.1\lib\FizzWare.NBuilder.dll + + + False + ..\packages\FluentAssertions.2.0.1\lib\net40\FluentAssertions.dll + ..\packages\NUnit.2.6.2\lib\nunit.framework.dll + + ..\packages\valueinjecter.2.3.3\lib\net35\Omu.ValueInjecter.dll + @@ -62,6 +72,8 @@ + + diff --git a/NzbDrone.Api.Test/packages.config b/NzbDrone.Api.Test/packages.config index 5c3ca54dd..f0a2d165c 100644 --- a/NzbDrone.Api.Test/packages.config +++ b/NzbDrone.Api.Test/packages.config @@ -1,4 +1,7 @@  + + + \ No newline at end of file diff --git a/NzbDrone.Api/Episodes/EpisodeResource.cs b/NzbDrone.Api/Episodes/EpisodeResource.cs index 485bc5333..d313ef1c3 100644 --- a/NzbDrone.Api/Episodes/EpisodeResource.cs +++ b/NzbDrone.Api/Episodes/EpisodeResource.cs @@ -1,10 +1,12 @@ using System; -using System.Linq; +using NzbDrone.Api.REST; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Model; +using NzbDrone.Core.Tv; namespace NzbDrone.Api.Episodes { - public class EpisodeResource + public class EpisodeResource : RestResource { public Int32 Id { get; set; } public Int32 SeriesId { get; set; } @@ -12,9 +14,19 @@ namespace NzbDrone.Api.Episodes public Int32 SeasonNumber { get; set; } public Int32 EpisodeNumber { get; set; } public String Title { get; set; } - public DateTime AirDate { get; set; } - public Int32 Status { get; set; } + public DateTime? AirDate { get; set; } + public EpisodeStatuses Status { get; set; } public String Overview { get; set; } public EpisodeFile EpisodeFile { get; set; } + + public Boolean HasFile { get; set; } + public Boolean Ignored { get; set; } + public Int32 SceneEpisodeNumber { get; set; } + public Int32 SceneSeasonNumber { get; set; } + public Int32 TvDbEpisodeId { get; set; } + public Int32? AbsoluteEpisodeNumber { get; set; } + public DateTime? EndTime { get; set; } + public DateTime? GrabDate { get; set; } + public PostDownloadStatusType PostDownloadStatus { get; set; } } } diff --git a/NzbDrone.Api/Mapping/MappingValidation.cs b/NzbDrone.Api/Mapping/MappingValidation.cs new file mode 100644 index 000000000..00cf1f681 --- /dev/null +++ b/NzbDrone.Api/Mapping/MappingValidation.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using System.Reflection; +using NzbDrone.Api.REST; +using NzbDrone.Common.Reflection; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Api.Mapping +{ + public static class MappingValidation + { + public static void ValidateMapping(Type modelType, Type resourceType) + { + var errors = modelType.GetSimpleProperties().Select(p => GetError(resourceType, p)).Where(c => c != null).ToList(); + + if (errors.Any()) + { + throw new ResourceMappingException(errors); + } + + PrintExtraProperties(modelType, resourceType); + + } + + + private static void PrintExtraProperties(Type modelType, Type resourceType) + { + var resourceBaseProperties = typeof(RestResource).GetProperties().Select(c => c.Name); + var resourceProperties = resourceType.GetProperties().Select(c => c.Name).Except(resourceBaseProperties); + var modelProperties = modelType.GetProperties().Select(c => c.Name); + + var extra = resourceProperties.Except(modelProperties); + + foreach (var extraProp in extra) + { + Console.WriteLine("Extra: [{0}]", extraProp); + } + + + } + + private static string GetError(Type resourceType, PropertyInfo modelProperty) + { + var resourceProperty = resourceType.GetProperties().FirstOrDefault(c => c.Name == modelProperty.Name); + + if (resourceProperty == null) + { + return string.Format("public {0} {1} {{ get; set; }}", modelProperty.PropertyType.Name, modelProperty.Name); + } + + if (resourceProperty.PropertyType != modelProperty.PropertyType) + { + return string.Format("Excpected {0}.{1} to have type of {2} but found {3}", resourceType.Name, resourceProperty.Name, modelProperty.PropertyType, resourceProperty.PropertyType); + } + + return null; + } + + } +} \ No newline at end of file diff --git a/NzbDrone.Api/Mapping/ResourceMappingException.cs b/NzbDrone.Api/Mapping/ResourceMappingException.cs new file mode 100644 index 000000000..90dae5e93 --- /dev/null +++ b/NzbDrone.Api/Mapping/ResourceMappingException.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NzbDrone.Api.Mapping +{ + public class ResourceMappingException : ApplicationException + { + public ResourceMappingException(IEnumerable error) + : base(Environment.NewLine + String.Join(Environment.NewLine, error.OrderBy(c => c))) + { + + } + } +} \ No newline at end of file diff --git a/NzbDrone.Api/Mapping/ValueInjectorExtensions.cs b/NzbDrone.Api/Mapping/ValueInjectorExtensions.cs new file mode 100644 index 000000000..481360662 --- /dev/null +++ b/NzbDrone.Api/Mapping/ValueInjectorExtensions.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Omu.ValueInjecter; + +namespace NzbDrone.Api.Mapping +{ + public static class ValueInjectorExtensions + { + public static TTarget InjectTo(this object source) where TTarget : new() + { + var targetType = typeof(TTarget); + + if (targetType.IsGenericType && + targetType.GetGenericTypeDefinition() != null && + targetType.GetGenericTypeDefinition().GetInterfaces().Contains(typeof(IEnumerable)) && + source.GetType().IsGenericType && + source.GetType().GetGenericTypeDefinition() != null && + source.GetType().GetGenericTypeDefinition().GetInterfaces().Contains(typeof(IEnumerable))) + { + + var result = new TTarget(); + + var listSubType = targetType.GetGenericArguments()[0]; + var listType = typeof(List<>).MakeGenericType(listSubType); + var addMethod = listType.GetMethod("Add"); + + foreach (var sourceItem in (IEnumerable)source) + { + var e = Activator.CreateInstance(listSubType).InjectFrom(sourceItem); + addMethod.Invoke(result, new[] { e }); + } + + return result; + } + + return (TTarget)new TTarget().InjectFrom(source); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Api/NzbDrone.Api.csproj b/NzbDrone.Api/NzbDrone.Api.csproj index 44701bd85..29e633ecf 100644 --- a/NzbDrone.Api/NzbDrone.Api.csproj +++ b/NzbDrone.Api/NzbDrone.Api.csproj @@ -88,6 +88,9 @@ + + + diff --git a/NzbDrone.Api/REST/RestModule.cs b/NzbDrone.Api/REST/RestModule.cs index 807030972..951650f83 100644 --- a/NzbDrone.Api/REST/RestModule.cs +++ b/NzbDrone.Api/REST/RestModule.cs @@ -40,7 +40,7 @@ namespace NzbDrone.Api.REST Get[ID_ROUTE] = options => { EnsureImplementation(GetResourceById); - var resource = GetResourceById(options.Id); + var resource = GetResourceById((int)options.Id); return resource.AsResponse(); }; @@ -61,7 +61,7 @@ namespace NzbDrone.Api.REST Delete[ID_ROUTE] = options => { EnsureImplementation(DeleteResource); - DeleteResource(options.Id); + DeleteResource((int)options.Id); return new Response { StatusCode = HttpStatusCode.OK }; }; diff --git a/NzbDrone.Api/Series/SeriesModule.cs b/NzbDrone.Api/Series/SeriesModule.cs index c792940f4..4dee24aef 100644 --- a/NzbDrone.Api/Series/SeriesModule.cs +++ b/NzbDrone.Api/Series/SeriesModule.cs @@ -1,14 +1,13 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; -using AutoMapper; using FluentValidation; using NzbDrone.Api.Extensions; using NzbDrone.Common; +using NzbDrone.Common; using NzbDrone.Core.SeriesStats; +using NzbDrone.Api.Mapping; using NzbDrone.Core.Tv; -using NzbDrone.Core.Model; using NzbDrone.Api.Validation; namespace NzbDrone.Api.Series @@ -43,7 +42,8 @@ namespace NzbDrone.Api.Series { var series = _seriesService.GetAllSeries().ToList(); var seriesStats = _seriesStatisticsService.SeriesStatistics(); - var seriesModels = Mapper.Map, List>(series); + + var seriesModels = series.InjectTo>(); foreach (var s in seriesModels) { @@ -52,7 +52,7 @@ namespace NzbDrone.Api.Series s.EpisodeCount = stats.EpisodeCount; s.EpisodeFileCount = stats.EpisodeFileCount; - s.NumberOfSeasons = stats.NumberOfSeasons; + s.SeasonsCount = stats.NumberOfSeasons; s.NextAiring = stats.NextAiring; } @@ -62,8 +62,7 @@ namespace NzbDrone.Api.Series private SeriesResource GetSeries(int id) { var series = _seriesService.GetSeries(id); - var seriesModels = Mapper.Map(series); - return seriesModels; + return series.InjectTo(); } private SeriesResource AddSeries(SeriesResource seriesResource) @@ -74,9 +73,9 @@ namespace NzbDrone.Api.Series //Todo: We need to create the folder if the user is adding a new series //(we can just create the folder and it won't blow up if it already exists) //We also need to remove any special characters from the filename before attempting to create it - var series = Mapper.Map(seriesResource); + var series = seriesResource.InjectTo(); _seriesService.AddSeries(series); - return Mapper.Map(series); + return series.InjectTo(); } private SeriesResource UpdateSeries(SeriesResource seriesResource) @@ -91,17 +90,11 @@ namespace NzbDrone.Api.Series series.RootFolderId = seriesResource.RootFolderId; series.FolderName = seriesResource.FolderName; - series.BacklogSetting = (BacklogSettingType)seriesResource.BacklogSetting; - - if (!String.IsNullOrWhiteSpace(seriesResource.CustomStartDate)) - series.CustomStartDate = DateTime.Parse(seriesResource.CustomStartDate, null, DateTimeStyles.RoundtripKind); - - else - series.CustomStartDate = null; + series.BacklogSetting = seriesResource.BacklogSetting; _seriesService.UpdateSeries(series); - return Mapper.Map(series); + return series.InjectTo(); } private void DeleteSeries(int id) @@ -111,23 +104,4 @@ namespace NzbDrone.Api.Series } } - public class SeriesValidator : AbstractValidator - { - private readonly DiskProvider _diskProvider; - - public SeriesValidator(DiskProvider diskProvider) - { - _diskProvider = diskProvider; - } - - public SeriesValidator() - { - RuleSet("POST", () => - { - RuleFor(s => s.Id).GreaterThan(0); - RuleFor(s => s.Path).NotEmpty().Must(_diskProvider.FolderExists); - RuleFor(s => s.QualityProfileId).GreaterThan(0); - }); - } - } -} \ No newline at end of file +} diff --git a/NzbDrone.Api/Series/SeriesResource.cs b/NzbDrone.Api/Series/SeriesResource.cs index 4583db6e6..3c681b18f 100644 --- a/NzbDrone.Api/Series/SeriesResource.cs +++ b/NzbDrone.Api/Series/SeriesResource.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using NzbDrone.Api.REST; +using NzbDrone.Core.Model; +using NzbDrone.Core.Tv; namespace NzbDrone.Api.Series { @@ -15,21 +17,17 @@ namespace NzbDrone.Api.Series public Int32 SeasonsCount { get; set; } public Int32 EpisodeCount { get; set; } public Int32 EpisodeFileCount { get; set; } - public String Status { get; set; } - public String AirsDayOfWeek { get; set; } + public SeriesStatusType Status { get; set; } public String QualityProfileName { get; set; } public String Overview { get; set; } - public Int32 Episodes { get; set; } - public Boolean HasBanner { get; set; } public DateTime? NextAiring { get; set; } - public String Details { get; set; } public String Network { get; set; } public String AirTime { get; set; } - public String Language { get; set; } - public Int32 NumberOfSeasons { get; set; } public Int32 UtcOffset { get; set; } public List Images { get; set; } + public String Path { get; set; } + //View & Edit public int RootFolderId { get; set; } public string FolderName { get; set; } @@ -38,7 +36,22 @@ namespace NzbDrone.Api.Series //Editing Only public Boolean SeasonFolder { get; set; } public Boolean Monitored { get; set; } - public Int32 BacklogSetting { get; set; } - public String CustomStartDate { get; set; } + public BacklogSettingType BacklogSetting { get; set; } + public DateTime? CustomStartDate { get; set; } + + public Boolean UseSceneNumbering { get; set; } + public Int32 Id { get; set; } + public Int32 Runtime { get; set; } + public Int32 TvdbId { get; set; } + public Int32 TvRageId { get; set; } + public DateTime? FirstAired { get; set; } + public DateTime? LastInfoSync { get; set; } + public SeriesTypes SeriesType { get; set; } + public String CleanTitle { get; set; } + public String ImdbId { get; set; } + public String TitleSlug { get; set; } + + + } } diff --git a/NzbDrone.Common/NzbDrone.Common.csproj b/NzbDrone.Common/NzbDrone.Common.csproj index 3013d0545..204ea9427 100644 --- a/NzbDrone.Common/NzbDrone.Common.csproj +++ b/NzbDrone.Common/NzbDrone.Common.csproj @@ -118,6 +118,7 @@ + diff --git a/NzbDrone.Common/Reflection/ReflectionExtensions.cs b/NzbDrone.Common/Reflection/ReflectionExtensions.cs new file mode 100644 index 000000000..8f89ba788 --- /dev/null +++ b/NzbDrone.Common/Reflection/ReflectionExtensions.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace NzbDrone.Common.Reflection +{ + public static class ReflectionExtensions + { + public static List GetSimpleProperties(this Type type) + { + var properties = type.GetProperties(); + return properties.Where(c => c.PropertyType.IsSimpleType()).ToList(); + } + + + public static List ImplementationsOf(this Assembly assembly) + { + return assembly.GetTypes().Where(c => typeof(T).IsAssignableFrom(c)).ToList(); + } + + + public static bool IsSimpleType(this Type type) + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + type = type.GetGenericArguments()[0]; + } + + return type.IsPrimitive + || type.IsEnum + || type == typeof(string) + || type == typeof(DateTime) + || type == typeof(Decimal); + } + + public static bool IsReadable(this PropertyInfo propertyInfo) + { + return propertyInfo.CanRead && propertyInfo.GetGetMethod(false) != null; + } + + public static bool IsWritable(this PropertyInfo propertyInfo) + { + return propertyInfo.CanWrite && propertyInfo.GetSetMethod(false) != null; + } + + } +} \ No newline at end of file diff --git a/NzbDrone.Core/Datastore/MappingExtensions.cs b/NzbDrone.Core/Datastore/MappingExtensions.cs index efd23f909..ccc940d5e 100644 --- a/NzbDrone.Core/Datastore/MappingExtensions.cs +++ b/NzbDrone.Core/Datastore/MappingExtensions.cs @@ -2,6 +2,7 @@ using System.Reflection; using Marr.Data; using Marr.Data.Mapping; +using NzbDrone.Common.Reflection; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Datastore @@ -43,7 +44,7 @@ namespace NzbDrone.Core.Datastore return false; } - if (IsSimpleType(propertyInfo.PropertyType) || MapRepository.Instance.TypeConverters.ContainsKey(propertyInfo.PropertyType)) + if (propertyInfo.PropertyType.IsSimpleType() || MapRepository.Instance.TypeConverters.ContainsKey(propertyInfo.PropertyType)) { return true; } @@ -51,28 +52,6 @@ namespace NzbDrone.Core.Datastore return false; } - public static bool IsSimpleType(Type type) - { - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - type = type.GetGenericArguments()[0]; - } - return type.IsPrimitive - || type.IsEnum - || type == typeof(string) - || type == typeof(DateTime) - || type == typeof(Decimal); - } - - private static bool IsReadable(this PropertyInfo propertyInfo) - { - return propertyInfo.CanRead && propertyInfo.GetGetMethod(false) != null; - } - - private static bool IsWritable(this PropertyInfo propertyInfo) - { - return propertyInfo.CanWrite && propertyInfo.GetSetMethod(false) != null; - } } } \ No newline at end of file diff --git a/UI/.idea/inspectionProfiles/Project_Default.xml b/UI/.idea/inspectionProfiles/Project_Default.xml index 126d9c3e6..0524d009e 100644 --- a/UI/.idea/inspectionProfiles/Project_Default.xml +++ b/UI/.idea/inspectionProfiles/Project_Default.xml @@ -66,6 +66,7 @@