1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-09-17 15:02:34 +02:00

moved seriesmodule to restmodule

This commit is contained in:
kay.one 2013-04-20 15:14:41 -07:00
parent 4afec69c79
commit d85b825e06
11 changed files with 142 additions and 43 deletions

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Linq; using System.Linq;
using FluentValidation;
using NLog; using NLog;
using Nancy; using Nancy;
using NzbDrone.Api.Extensions; using NzbDrone.Api.Extensions;
@ -25,6 +26,17 @@ public Response HandleException(NancyContext context, Exception exception)
return apiException.ToErrorResponse(); return apiException.ToErrorResponse();
} }
var validationException = exception as ValidationException;
if (validationException != null)
{
_logger.Warn("Invalid request {0}", validationException.Message);
return validationException.Errors.AsResponse(HttpStatusCode.BadRequest);
}
_logger.ErrorException("Unexpected error", exception); _logger.ErrorException("Unexpected error", exception);

View File

@ -90,9 +90,11 @@
<Compile Include="Frontend\StaticResourceMapper.cs" /> <Compile Include="Frontend\StaticResourceMapper.cs" />
<Compile Include="Missing\MissingResource.cs" /> <Compile Include="Missing\MissingResource.cs" />
<Compile Include="Missing\MissingModule.cs" /> <Compile Include="Missing\MissingModule.cs" />
<Compile Include="NzbDroneRestModule.cs" />
<Compile Include="Resolvers\EndTimeResolver.cs" /> <Compile Include="Resolvers\EndTimeResolver.cs" />
<Compile Include="Resolvers\NextAiringResolver.cs" /> <Compile Include="Resolvers\NextAiringResolver.cs" />
<Compile Include="Resolvers\NullableDatetimeToString.cs" /> <Compile Include="Resolvers\NullableDatetimeToString.cs" />
<Compile Include="REST\ResourceValidator.cs" />
<Compile Include="REST\RestModule.cs" /> <Compile Include="REST\RestModule.cs" />
<Compile Include="REST\RestResource.cs" /> <Compile Include="REST\RestResource.cs" />
<Compile Include="RootFolders\RootFolderModule.cs" /> <Compile Include="RootFolders\RootFolderModule.cs" />
@ -118,6 +120,7 @@
<Compile Include="Resolvers\QualityTypesToIntResolver.cs" /> <Compile Include="Resolvers\QualityTypesToIntResolver.cs" />
<Compile Include="Settings\SettingsModule.cs" /> <Compile Include="Settings\SettingsModule.cs" />
<Compile Include="TinyIoCNancyBootstrapper.cs" /> <Compile Include="TinyIoCNancyBootstrapper.cs" />
<Compile Include="Validation\IdValidationRule.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />

View File

@ -0,0 +1,16 @@
using NzbDrone.Api.REST;
using NzbDrone.Api.Validation;
namespace NzbDrone.Api
{
public abstract class NzbDroneRestModule<TResource> : RestModule<TResource> where TResource : RestResource, new()
{
protected NzbDroneRestModule(string resource)
: base("/api/" + resource.Trim('/'))
{
PostValidator.RuleFor(r => r.Id).IsZero();
PutValidator.RuleFor(r => r.Id).ValidId();
}
}
}

View File

@ -0,0 +1,9 @@
using FluentValidation;
namespace NzbDrone.Api.REST
{
public class ResourceValidator<TResource> : AbstractValidator<TResource>
{
}
}

View File

@ -1,13 +1,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using FluentValidation;
using Nancy; using Nancy;
using NzbDrone.Api.Extensions; using NzbDrone.Api.Extensions;
using System.Linq;
namespace NzbDrone.Api.REST namespace NzbDrone.Api.REST
{ {
public abstract class RestModule<TResource> : NancyModule public abstract class RestModule<TResource> : NancyModule
where TResource : RestResource, new() where TResource : RestResource, new()
{ {
protected ResourceValidator<TResource> PostValidator { get; private set; }
protected ResourceValidator<TResource> PutValidator { get; private set; }
protected ResourceValidator<TResource> SharedValidator { get; private set; }
private const string ROOT_ROUTE = "/"; private const string ROOT_ROUTE = "/";
private const string ID_ROUTE = "/{id}"; private const string ID_ROUTE = "/{id}";
@ -20,6 +25,11 @@ protected RestModule()
protected RestModule(string modulePath) protected RestModule(string modulePath)
: base(modulePath) : base(modulePath)
{ {
PostValidator = new ResourceValidator<TResource>();
PutValidator = new ResourceValidator<TResource>();
SharedValidator = new ResourceValidator<TResource>();
Get[ROOT_ROUTE] = options => Get[ROOT_ROUTE] = options =>
{ {
EnsureImplementation(GetResourceAll); EnsureImplementation(GetResourceAll);
@ -55,7 +65,7 @@ protected RestModule(string modulePath)
return new Response { StatusCode = HttpStatusCode.OK }; return new Response { StatusCode = HttpStatusCode.OK };
}; };
} }
@ -78,13 +88,21 @@ private TResource ReadFromRequest()
{ {
var resource = Request.Body.FromJson<TResource>(); var resource = Request.Body.FromJson<TResource>();
var errors = SharedValidator.Validate(resource).Errors.ToList();
if (Request.Method.Equals("POST", StringComparison.InvariantCultureIgnoreCase)) if (Request.Method.Equals("POST", StringComparison.InvariantCultureIgnoreCase))
{ {
//resource.ValidateForPost(); errors.AddRange(PostValidator.Validate(resource).Errors);
} }
else if (Request.Method.Equals("PUT", StringComparison.InvariantCultureIgnoreCase)) else if (Request.Method.Equals("PUT", StringComparison.InvariantCultureIgnoreCase))
{ {
//resource.ValidateForPut(); errors.AddRange(PutValidator.Validate(resource).Errors);
}
if (errors.Any())
{
throw new ValidationException(errors);
} }
return resource; return resource;

View File

@ -4,15 +4,15 @@
using System.Linq; using System.Linq;
using AutoMapper; using AutoMapper;
using FluentValidation; using FluentValidation;
using Nancy;
using NzbDrone.Api.Extensions; using NzbDrone.Api.Extensions;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Core.Model; using NzbDrone.Core.Model;
using NzbDrone.Api.Validation;
namespace NzbDrone.Api.Series namespace NzbDrone.Api.Series
{ {
public class SeriesModule : NzbDroneApiModule//: RestModule<SeriesResource> public class SeriesModule : NzbDroneRestModule<SeriesResource>
{ {
private readonly ISeriesService _seriesService; private readonly ISeriesService _seriesService;
@ -20,15 +20,23 @@ public SeriesModule(ISeriesService seriesService)
: base("/Series") : base("/Series")
{ {
_seriesService = seriesService; _seriesService = seriesService;
Get["/"] = x => AllSeries();
Get["/{id}"] = x => GetSeries((int)x.id);
Post["/"] = x => AddSeries();
Put["/"] = x => UpdateSeries();
Delete["/{id}"] = x => DeleteSeries((int)x.id); GetResourceAll = AllSeries;
GetResourceById = GetSeries;
CreateResource = AddSeries;
UpdateResource = UpdateSeries;
DeleteResource = DeleteSeries;
SharedValidator.RuleFor(s => s.RootFolderId).ValidId();
SharedValidator.RuleFor(s => s.QualityProfileId).ValidId();
PostValidator.RuleFor(s => s.Title).NotEmpty();
} }
private Response AllSeries() private List<SeriesResource> AllSeries()
{ {
var series = _seriesService.GetAllSeries().ToList(); var series = _seriesService.GetAllSeries().ToList();
var seriesStats = _seriesService.SeriesStatistics(); var seriesStats = _seriesService.SeriesStatistics();
@ -45,18 +53,17 @@ private Response AllSeries()
s.NextAiring = stats.NextAiring; s.NextAiring = stats.NextAiring;
} }
return seriesModels.AsResponse(); return seriesModels;
} }
private Response GetSeries(int id) private SeriesResource GetSeries(int id)
{ {
var series = _seriesService.GetSeries(id); var series = _seriesService.GetSeries(id);
var seriesModels = Mapper.Map<Core.Tv.Series, SeriesResource>(series); var seriesModels = Mapper.Map<Core.Tv.Series, SeriesResource>(series);
return seriesModels;
return seriesModels.AsResponse();
} }
private Response AddSeries() private SeriesResource AddSeries(SeriesResource seriesResource)
{ {
var newSeries = Request.Body.FromJson<Core.Tv.Series>(); var newSeries = Request.Body.FromJson<Core.Tv.Series>();
@ -64,44 +71,40 @@ private Response AddSeries()
//Todo: We need to create the folder if the user is adding a new 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 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 //We also need to remove any special characters from the filename before attempting to create it
var series = Mapper.Map<SeriesResource, Core.Tv.Series>(seriesResource);
_seriesService.AddSeries(newSeries); _seriesService.AddSeries(series);
return Mapper.Map<Core.Tv.Series, SeriesResource>(series);
return new Response { StatusCode = HttpStatusCode.Created };
} }
private Response UpdateSeries() private SeriesResource UpdateSeries(SeriesResource seriesResource)
{ {
var request = Request.Body.FromJson<SeriesResource>(); var series = _seriesService.GetSeries(seriesResource.Id);
var series = _seriesService.GetSeries(request.Id); series.Monitored = seriesResource.Monitored;
series.SeasonFolder = seriesResource.SeasonFolder;
series.Monitored = request.Monitored; series.QualityProfileId = seriesResource.QualityProfileId;
series.SeasonFolder = request.SeasonFolder;
series.QualityProfileId = request.QualityProfileId;
//Todo: Do we want to force a scan when this path changes? Can we use events instead? //Todo: Do we want to force a scan when this path changes? Can we use events instead?
series.RootFolderId = request.RootFolderId; series.RootFolderId = seriesResource.RootFolderId;
series.FolderName = request.FolderName; series.FolderName = seriesResource.FolderName;
series.BacklogSetting = (BacklogSettingType)request.BacklogSetting; series.BacklogSetting = (BacklogSettingType)seriesResource.BacklogSetting;
if (!String.IsNullOrWhiteSpace(request.CustomStartDate)) if (!String.IsNullOrWhiteSpace(seriesResource.CustomStartDate))
series.CustomStartDate = DateTime.Parse(request.CustomStartDate, null, DateTimeStyles.RoundtripKind); series.CustomStartDate = DateTime.Parse(seriesResource.CustomStartDate, null, DateTimeStyles.RoundtripKind);
else else
series.CustomStartDate = null; series.CustomStartDate = null;
_seriesService.UpdateSeries(series); _seriesService.UpdateSeries(series);
return request.AsResponse(); return Mapper.Map<Core.Tv.Series, SeriesResource>(series);
} }
private Response DeleteSeries(int id) private void DeleteSeries(int id)
{ {
var deleteFiles = Convert.ToBoolean(Request.Headers["deleteFiles"].FirstOrDefault()); var deleteFiles = Convert.ToBoolean(Request.Headers["deleteFiles"].FirstOrDefault());
_seriesService.DeleteSeries(id, deleteFiles); _seriesService.DeleteSeries(id, deleteFiles);
return new Response { StatusCode = HttpStatusCode.OK };
} }
} }

View File

@ -0,0 +1,19 @@
using FluentValidation;
using FluentValidation.Validators;
namespace NzbDrone.Api.Validation
{
public static class RuleBuilderExtensions
{
public static IRuleBuilderOptions<T, int> ValidId<T>(this IRuleBuilder<T, int> ruleBuilder)
{
return ruleBuilder.SetValidator(new GreaterThanValidator(0));
}
public static IRuleBuilderOptions<T, int> IsZero<T>(this IRuleBuilder<T, int> ruleBuilder)
{
return ruleBuilder.SetValidator(new EqualValidator(0));
}
}
}

View File

@ -1,7 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using FluentAssertions; using FluentAssertions;
using FluentValidation;
using FluentValidation.Results;
using NLog; using NLog;
using Newtonsoft.Json;
using RestSharp; using RestSharp;
namespace NzbDrone.Integration.Test.Client namespace NzbDrone.Integration.Test.Client
@ -33,6 +36,13 @@ public TResource Post(TResource body)
return Post<TResource>(request); return Post<TResource>(request);
} }
public List<string> InvalidPost(TResource body)
{
var request = BuildRequest();
request.AddBody(body);
return Post<List<string>>(request, HttpStatusCode.BadRequest);
}
protected RestRequest BuildRequest(string command = "") protected RestRequest BuildRequest(string command = "")
{ {
return new RestRequest(_resource + "/" + command.Trim('/')) return new RestRequest(_resource + "/" + command.Trim('/'))
@ -58,9 +68,10 @@ protected RestRequest BuildRequest(string command = "")
_logger.Info("{0}: {1}", request.Method, _restClient.BuildUri(request)); _logger.Info("{0}: {1}", request.Method, _restClient.BuildUri(request));
var response = _restClient.Execute<T>(request); var response = _restClient.Execute<T>(request);
_logger.Info("Response: {0}", response.Content); _logger.Info("Response: {0}", response.Content);
response.StatusCode.Should().Be(statusCode);
if (response.ErrorException != null) if (response.ErrorException != null)
{ {
throw response.ErrorException; throw response.ErrorException;
@ -68,9 +79,6 @@ protected RestRequest BuildRequest(string command = "")
response.ErrorMessage.Should().BeBlank(); response.ErrorMessage.Should().BeBlank();
response.StatusCode.Should().Be(statusCode);
return response.Data; return response.Data;
} }

View File

@ -42,12 +42,20 @@
<Reference Include="FluentAssertions"> <Reference Include="FluentAssertions">
<HintPath>..\packages\FluentAssertions.2.0.1\lib\net40\FluentAssertions.dll</HintPath> <HintPath>..\packages\FluentAssertions.2.0.1\lib\net40\FluentAssertions.dll</HintPath>
</Reference> </Reference>
<Reference Include="FluentValidation, Version=3.4.6.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\FluentValidation.3.4.6.0\lib\Net40\FluentValidation.dll</HintPath>
</Reference>
<Reference Include="Nancy"> <Reference Include="Nancy">
<HintPath>..\packages\Nancy.0.16.1\lib\net40\Nancy.dll</HintPath> <HintPath>..\packages\Nancy.0.16.1\lib\net40\Nancy.dll</HintPath>
</Reference> </Reference>
<Reference Include="Nancy.Hosting.Self"> <Reference Include="Nancy.Hosting.Self">
<HintPath>..\packages\Nancy.Hosting.Self.0.16.1\lib\net40\Nancy.Hosting.Self.dll</HintPath> <HintPath>..\packages\Nancy.Hosting.Self.0.16.1\lib\net40\Nancy.Hosting.Self.dll</HintPath>
</Reference> </Reference>
<Reference Include="Newtonsoft.Json, Version=4.5.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Newtonsoft.Json.5.0.3\lib\net40\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="NLog"> <Reference Include="NLog">
<HintPath>..\packages\NLog.2.0.1.2\lib\net40\NLog.dll</HintPath> <HintPath>..\packages\NLog.2.0.1.2\lib\net40\NLog.dll</HintPath>
</Reference> </Reference>

View File

@ -1,4 +1,5 @@
using FluentAssertions; using System.Net;
using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Api.Series; using NzbDrone.Api.Series;
@ -23,10 +24,10 @@ public void series_lookup_on_trakt()
} }
[Test] [Test]
[Ignore] public void add_series_without_required_fields_should_return_badrequest()
public void add_series_without_required_fields_should_return_400()
{ {
Series.Post(new SeriesResource()); var errors = Series.InvalidPost(new SeriesResource());
errors.Should().NotBeEmpty();
} }
} }

View File

@ -3,8 +3,10 @@
<package id="Exceptron.Client" version="1.0.20" targetFramework="net40" /> <package id="Exceptron.Client" version="1.0.20" targetFramework="net40" />
<package id="Exceptron.NLog" version="1.0.11" targetFramework="net40" /> <package id="Exceptron.NLog" version="1.0.11" targetFramework="net40" />
<package id="FluentAssertions" version="2.0.1" targetFramework="net40" /> <package id="FluentAssertions" version="2.0.1" targetFramework="net40" />
<package id="FluentValidation" version="3.4.6.0" targetFramework="net40" />
<package id="Nancy" version="0.16.1" targetFramework="net40" /> <package id="Nancy" version="0.16.1" targetFramework="net40" />
<package id="Nancy.Hosting.Self" version="0.16.1" targetFramework="net40" /> <package id="Nancy.Hosting.Self" version="0.16.1" targetFramework="net40" />
<package id="Newtonsoft.Json" version="5.0.3" targetFramework="net40" />
<package id="NLog" version="2.0.1.2" targetFramework="net40" /> <package id="NLog" version="2.0.1.2" targetFramework="net40" />
<package id="NUnit" version="2.6.2" targetFramework="net40" /> <package id="NUnit" version="2.6.2" targetFramework="net40" />
<package id="RestSharp" version="104.1" targetFramework="net40" /> <package id="RestSharp" version="104.1" targetFramework="net40" />