using System; using System.Collections.Generic; using System.Linq; using FluentValidation; using Lidarr.Http.Extensions; using Nancy; using Nancy.Responses.Negotiation; using Newtonsoft.Json; using NzbDrone.Core.Datastore; namespace Lidarr.Http.REST { public abstract class RestModule : NancyModule where TResource : RestResource, new() { private const string ROOT_ROUTE = "/"; private const string ID_ROUTE = @"/(?[\d]{1,10})"; private readonly HashSet _excludedKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase) { "page", "pageSize", "sortKey", "sortDirection", "filterKey", "filterValue", }; private Action _deleteResource; private Func _getResourceById; private Func> _getResourceAll; private Func, PagingResource> _getResourcePaged; private Func _getResourceSingle; private Func _createResource; private Action _updateResource; protected ResourceValidator PostValidator { get; private set; } protected ResourceValidator PutValidator { get; private set; } protected ResourceValidator SharedValidator { get; private set; } protected void ValidateId(int id) { if (id <= 0) { throw new BadRequestException(id + " is not a valid ID"); } } protected RestModule(string modulePath) : base(modulePath) { ValidateModule(); PostValidator = new ResourceValidator(); PutValidator = new ResourceValidator(); SharedValidator = new ResourceValidator(); } private void ValidateModule() { if (GetResourceById != null) { return; } if (CreateResource != null || UpdateResource != null) { throw new InvalidOperationException("GetResourceById route must be defined before defining Create/Update routes."); } } protected Action DeleteResource { private get { return _deleteResource; } set { _deleteResource = value; Delete(ID_ROUTE, options => { ValidateId(options.Id); DeleteResource((int)options.Id); return new object(); }); } } protected Func GetResourceById { get { return _getResourceById; } set { _getResourceById = value; Get(ID_ROUTE, options => { ValidateId(options.Id); try { var resource = GetResourceById((int)options.Id); if (resource == null) { return new NotFoundResponse(); } return resource; } catch (ModelNotFoundException) { return new NotFoundResponse(); } }); } } protected Func> GetResourceAll { private get { return _getResourceAll; } set { _getResourceAll = value; Get(ROOT_ROUTE, options => { var resource = GetResourceAll(); return resource; }); } } protected Func, PagingResource> GetResourcePaged { private get { return _getResourcePaged; } set { _getResourcePaged = value; Get(ROOT_ROUTE, options => { var resource = GetResourcePaged(ReadPagingResourceFromRequest()); return resource; }); } } protected Func GetResourceSingle { private get { return _getResourceSingle; } set { _getResourceSingle = value; Get(ROOT_ROUTE, options => { var resource = GetResourceSingle(); return resource; }); } } protected Func CreateResource { private get { return _createResource; } set { _createResource = value; Post(ROOT_ROUTE, options => { var id = CreateResource(ReadResourceFromRequest()); return ResponseWithCode(GetResourceById(id), HttpStatusCode.Created); }); } } protected Action UpdateResource { private get { return _updateResource; } set { _updateResource = value; Put(ROOT_ROUTE, options => { var resource = ReadResourceFromRequest(); UpdateResource(resource); return ResponseWithCode(GetResourceById(resource.Id), HttpStatusCode.Accepted); }); Put(ID_ROUTE, options => { var resource = ReadResourceFromRequest(); resource.Id = options.Id; UpdateResource(resource); return ResponseWithCode(GetResourceById(resource.Id), HttpStatusCode.Accepted); }); } } protected Negotiator ResponseWithCode(object model, HttpStatusCode statusCode) { return Negotiate.WithModel(model).WithStatusCode(statusCode); } protected TResource ReadResourceFromRequest(bool skipValidate = false) { var resource = new TResource(); try { resource = Request.Body.FromJson(); } catch (JsonReaderException ex) { throw new BadRequestException(ex.Message); } if (resource == null) { throw new BadRequestException("Request body can't be empty"); } var errors = SharedValidator.Validate(resource).Errors.ToList(); if (Request.Method.Equals("POST", StringComparison.InvariantCultureIgnoreCase) && !skipValidate && !Request.Url.Path.EndsWith("/test", StringComparison.InvariantCultureIgnoreCase)) { errors.AddRange(PostValidator.Validate(resource).Errors); } else if (Request.Method.Equals("PUT", StringComparison.InvariantCultureIgnoreCase)) { errors.AddRange(PutValidator.Validate(resource).Errors); } if (errors.Any()) { throw new ValidationException(errors); } return resource; } private PagingResource ReadPagingResourceFromRequest() { int pageSize; int.TryParse(Request.Query.PageSize.ToString(), out pageSize); if (pageSize == 0) { pageSize = 10; } int page; int.TryParse(Request.Query.Page.ToString(), out page); if (page == 0) { page = 1; } var pagingResource = new PagingResource { PageSize = pageSize, Page = page, Filters = new List() }; if (Request.Query.SortKey != null) { pagingResource.SortKey = Request.Query.SortKey.ToString(); // For backwards compatibility with v2 if (Request.Query.SortDir != null) { pagingResource.SortDirection = Request.Query.SortDir.ToString() .Equals("Asc", StringComparison.InvariantCultureIgnoreCase) ? SortDirection.Ascending : SortDirection.Descending; } // v3 uses SortDirection instead of SortDir to be consistent with every other use of it if (Request.Query.SortDirection != null) { pagingResource.SortDirection = Request.Query.SortDirection.ToString() .Equals("ascending", StringComparison.InvariantCultureIgnoreCase) ? SortDirection.Ascending : SortDirection.Descending; } } // For backwards compatibility with v2 if (Request.Query.FilterKey != null) { var filter = new PagingResourceFilter { Key = Request.Query.FilterKey.ToString() }; if (Request.Query.FilterValue != null) { filter.Value = Request.Query.FilterValue?.ToString(); } pagingResource.Filters.Add(filter); } // v3 uses filters in key=value format foreach (var key in Request.Query) { if (_excludedKeys.Contains(key)) { continue; } pagingResource.Filters.Add(new PagingResourceFilter { Key = key, Value = Request.Query[key] }); } return pagingResource; } } }