diff --git a/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs b/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs index 2f5f74f..3cfce24 100644 --- a/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs @@ -144,7 +144,7 @@ public async Task Get_RootFlat_ReturnsEntryPointFlat_WhenAuthAndHeader() collection!.Id.Should().Be($"http://localhost/1/collections/{RootCollection.Id}"); collection.PublicId.Should().Be("http://localhost/1"); collection.Items!.Count.Should().Be(2); - collection.Items[0].Id.Should().Be("http://localhost/1/collections/FirstChildCollection"); + collection.Items.OfType().First().Id.Should().Be("http://localhost/1/collections/FirstChildCollection"); collection.TotalItems.Should().Be(2); collection.CreatedBy.Should().Be("admin"); collection.Behavior.Should().Contain("public-iiif"); @@ -167,7 +167,7 @@ public async Task Get_ChildFlat_ReturnsEntryPointFlat_WhenCalledByChildId() collection!.Id.Should().Be("http://localhost/1/collections/FirstChildCollection"); collection.PublicId.Should().Be("http://localhost/1/first-child"); collection.Items!.Count.Should().Be(1); - collection.Items[0].Id.Should().Be("http://localhost/1/collections/SecondChildCollection"); + collection.Items.OfType().First().Id.Should().Be("http://localhost/1/collections/SecondChildCollection"); collection.TotalItems.Should().Be(1); collection.CreatedBy.Should().Be("admin"); collection.Behavior.Should().Contain("public-iiif"); @@ -236,7 +236,7 @@ public async Task Get_ChildFlat_ReturnsReducedItems_WhenCalledWithSmallPageSize( collection.View.Page.Should().Be(1); collection.View.TotalPages.Should().Be(2); collection.Items!.Count.Should().Be(1); - collection.Items[0].Id.Should().Be("http://localhost/1/collections/FirstChildCollection"); + collection.Items.OfType().First().Id.Should().Be("http://localhost/1/collections/FirstChildCollection"); } [Fact] @@ -257,7 +257,7 @@ public async Task Get_RootFlat_ReturnsSecondPage_WhenCalledWithSmallPageSize() collection.View.Page.Should().Be(2); collection.View.TotalPages.Should().Be(2); collection.Items!.Count.Should().Be(1); - collection.Items[0].Id.Should().Be("http://localhost/1/collections/NonPublic"); + collection.Items.OfType().First().Id.Should().Be("http://localhost/1/collections/NonPublic"); } [Fact] @@ -322,7 +322,7 @@ public async Task Get_ChildFlat_ReturnsCorrectItem_WhenCalledWithSmallPageSizeAn collection.View.Page.Should().Be(1); collection.View.TotalPages.Should().Be(2); collection.Items!.Count.Should().Be(1); - collection.Items[0].Id.Should().Be("http://localhost/1/collections/FirstChildCollection"); + collection.Items.OfType().First().Id.Should().Be("http://localhost/1/collections/FirstChildCollection"); } [Theory] @@ -348,7 +348,7 @@ public async Task Get_RootFlat_ReturnsFirstPageWithSecondItem_WhenCalledWithSmal collection.View.Page.Should().Be(1); collection.View.TotalPages.Should().Be(2); collection.Items!.Count.Should().Be(1); - collection.Items[0].Id.Should().Be("http://localhost/1/collections/NonPublic"); + collection.Items.OfType().First().Id.Should().Be("http://localhost/1/collections/NonPublic"); } [Fact] @@ -371,6 +371,6 @@ public async Task Get_ChildFlat_IgnoresOrderBy_WhenCalledWithInvalidOrderBy() collection.View.Page.Should().Be(1); collection.View.TotalPages.Should().Be(2); collection.Items!.Count.Should().Be(1); - collection.Items[0].Id.Should().Be("http://localhost/1/collections/FirstChildCollection"); + collection.Items.OfType().First().Id.Should().Be("http://localhost/1/collections/FirstChildCollection"); } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/API.csproj b/src/IIIFPresentation/API/API.csproj index f06703f..ad4306a 100644 --- a/src/IIIFPresentation/API/API.csproj +++ b/src/IIIFPresentation/API/API.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/IIIFPresentation/API/Converters/CollectionConverter.cs b/src/IIIFPresentation/API/Converters/CollectionConverter.cs index 8ce5370..db6ea1b 100644 --- a/src/IIIFPresentation/API/Converters/CollectionConverter.cs +++ b/src/IIIFPresentation/API/Converters/CollectionConverter.cs @@ -9,20 +9,22 @@ namespace API.Converters; public static class CollectionConverter { - public static Collection ToHierarchicalCollection(this Models.Database.Collections.Collection dbAsset, + public static Collection ToHierarchicalCollection(this Models.Database.Collections.Collection dbAsset, UrlRoots urlRoots, List? items) { var collection = new Collection() { Id = dbAsset.GenerateHierarchicalCollectionId(urlRoots), Label = dbAsset.Label, - Items = items != null ? items.Select(x => new Collection() - { - Id = x.GenerateHierarchicalCollectionId(urlRoots), - Label = x.Label - }).ToList() : [] + Items = items != null + ? items.Select(x => new Collection() + { + Id = x.GenerateHierarchicalCollectionId(urlRoots), + Label = x.Label + }).ToList() + : [] }; - + collection.EnsurePresentation3Context(); return collection; @@ -32,11 +34,11 @@ public static FlatCollection ToFlatCollection(this Models.Database.Collections.C UrlRoots urlRoots, int pageSize, int currentPage, int totalItems, List? items, string? orderQueryParam = null) { - var totalPages = (int)Math.Ceiling(totalItems == 0 ? 1 : (double)totalItems / pageSize); + var totalPages = (int) Math.Ceiling(totalItems == 0 ? 1 : (double) totalItems / pageSize); var orderQueryParamConverted = string.IsNullOrEmpty(orderQueryParam) ? string.Empty : $"&{orderQueryParam}"; - - return new FlatCollection() + + return new() { Id = dbAsset.GenerateFlatCollectionId(urlRoots), Context = new List @@ -49,7 +51,6 @@ public static FlatCollection ToFlatCollection(this Models.Database.Collections.C Behavior = new List() .AppendIf(dbAsset.IsPublic, Behavior.IsPublic) .AppendIf(dbAsset.IsStorageCollection, Behavior.IsStorageCollection), - Type = PresentationType.Collection, Slug = dbAsset.Slug, Parent = dbAsset.Parent != null ? dbAsset.GenerateFlatCollectionParent(urlRoots) @@ -57,24 +58,22 @@ public static FlatCollection ToFlatCollection(this Models.Database.Collections.C ItemsOrder = dbAsset.ItemsOrder, Items = items != null - ? items.Select(i => new Item + ? items.Select(i => (ICollectionItem) new Collection() { Id = i.GenerateFlatCollectionId(urlRoots), - Label = i.Label, - Type = PresentationType.Collection + Label = i.Label }).ToList() : [], PartOf = dbAsset.Parent != null - ? new List - { - new() + ? + [ + new PartOf(nameof(PresentationType.Collection)) { Id = $"{urlRoots.BaseUrl}/{dbAsset.CustomerId}/{dbAsset.Parent}", - Label = dbAsset.Label, - Type = PresentationType.Collection + Label = dbAsset.Label } - } + ] : null, TotalItems = totalItems, @@ -83,20 +82,18 @@ public static FlatCollection ToFlatCollection(this Models.Database.Collections.C SeeAlso = [ - new() + new(nameof(PresentationType.Collection)) { Id = dbAsset.GenerateHierarchicalCollectionId(urlRoots), - Type = PresentationType.Collection, Label = dbAsset.Label, - Profile = ["Public"] + Profile = "Public" }, - new() + new(nameof(PresentationType.Collection)) { Id = $"{dbAsset.GenerateHierarchicalCollectionId(urlRoots)}/iiif", - Type = PresentationType.Collection, Label = dbAsset.Label, - Profile = ["api-hierarchical"] + Profile = "api-hierarchical" } ], @@ -123,7 +120,8 @@ private static View GenerateView(Models.Database.Collections.Collection dbAsset, if (currentPage > 1) { view.First = dbAsset.GenerateFlatCollectionViewFirst(urlRoots, pageSize, orderQueryParam); - view.Previous = dbAsset.GenerateFlatCollectionViewPrevious(urlRoots, currentPage, pageSize, orderQueryParam); + view.Previous = + dbAsset.GenerateFlatCollectionViewPrevious(urlRoots, currentPage, pageSize, orderQueryParam); } if (totalPages > currentPage) @@ -131,7 +129,7 @@ private static View GenerateView(Models.Database.Collections.Collection dbAsset, view.Next = dbAsset.GenerateFlatCollectionViewNext(urlRoots, currentPage, pageSize, orderQueryParam); view.Last = dbAsset.GenerateFlatCollectionViewLast(urlRoots, totalPages, pageSize, orderQueryParam); } - + return view; } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Program.cs b/src/IIIFPresentation/API/Program.cs index 243b3a1..710e9e3 100644 --- a/src/IIIFPresentation/API/Program.cs +++ b/src/IIIFPresentation/API/Program.cs @@ -1,9 +1,14 @@ +using System.Runtime.Serialization; using System.Text.Json.Serialization; using API.Features.Storage.Validators; using API.Infrastructure; using API.Settings; +using Core.Response; +using IIIF.Presentation.V3; using Microsoft.AspNetCore.HttpOverrides; +using Models.API.Collection; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Repository; using Serilog; @@ -38,7 +43,7 @@ opts.ForwardedHeaders = ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto; }); -builder.Services.ConfigureHttpJsonOptions( options => +builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.WriteIndented = true; options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; diff --git a/src/IIIFPresentation/Core/Core.csproj b/src/IIIFPresentation/Core/Core.csproj index 826a536..9b9a652 100644 --- a/src/IIIFPresentation/Core/Core.csproj +++ b/src/IIIFPresentation/Core/Core.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs b/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs index 62cdc21..1b29692 100644 --- a/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs +++ b/src/IIIFPresentation/Core/Response/HttpResponseMessageX.cs @@ -1,4 +1,5 @@ -using Core.Exceptions; +using System.Runtime.Serialization; +using Core.Exceptions; using IIIF; using IIIF.Serialisation; using Newtonsoft.Json; @@ -7,6 +8,8 @@ namespace Core.Response; public static class HttpResponseMessageX { + public static StreamingContext? SerializationContext { get; set; } + /// /// Read HttpResponseMessage as JSON using Newtonsoft for conversion. /// @@ -23,10 +26,10 @@ public static class HttpResponseMessageX if (!response.IsJsonResponse()) return default; var contentStream = await response.Content.ReadAsStreamAsync(); - + return contentStream.FromJsonStream(); } - + /// /// Read HttpResponseMessage as JSON using Newtonsoft for conversion. /// @@ -36,29 +39,28 @@ public static class HttpResponseMessageX /// Type to convert response to /// Converted Http response. public static async Task ReadAsPresentationJsonAsync(this HttpResponseMessage response, - bool ensureSuccess = true, JsonSerializerSettings? settings = null) + bool ensureSuccess = true, JsonSerializerSettings? settings = null) where T : new() { if (ensureSuccess) response.EnsureSuccessStatusCode(); if (!response.IsJsonResponse()) return default; var contentStream = await response.Content.ReadAsStreamAsync(); - + using var streamReader = new StreamReader(contentStream); - using var jsonReader = new JsonTextReader(streamReader); + await using var jsonReader = new JsonTextReader(streamReader); - JsonSerializer serializer = new(); - if (settings == null) return serializer.Deserialize(jsonReader); - - if (settings.ContractResolver != null) - { - serializer.ContractResolver = settings.ContractResolver; - } - serializer.NullValueHandling = settings.NullValueHandling; - - return serializer.Deserialize(jsonReader); + settings ??= new(IIIFSerialiserX.DeserializerSettings); + if (SerializationContext.HasValue) + settings.Context = SerializationContext.Value; + + var serializer = JsonSerializer.Create(settings); + + var result = new T(); + serializer.Populate(jsonReader, result); + return result; } - + /// /// Check if the object contains a JSON response /// e.g. application/json, application/ld+json @@ -70,15 +72,15 @@ public static bool IsJsonResponse(this HttpResponseMessage response) var mediaType = response.Content.Headers.ContentType?.MediaType; return mediaType != null && mediaType.Contains("json"); } - + public static async Task ReadAsIIIFResponseAsync(this HttpResponseMessage response, JsonSerializerSettings? settings = null) where T : JsonLdBase { - if ((int)response.StatusCode < 400) + if ((int) response.StatusCode < 400) { return await response.ReadWithIIIFContext(true, settings); } - + try { return await response.ReadAsIIIFJsonAsync(false, settings); @@ -88,15 +90,15 @@ public static bool IsJsonResponse(this HttpResponseMessage response) throw new PresentationException("Could not convert response JSON to error", ex); } } - + public static async Task ReadAsPresentationResponseAsync(this HttpResponseMessage response, - JsonSerializerSettings? settings = null) + JsonSerializerSettings? settings = null) where T : new() { - if ((int)response.StatusCode < 400) + if ((int) response.StatusCode < 400) { return await response.ReadWithContext(true, settings); } - + try { return await response.ReadAsPresentationJsonAsync(false, settings); @@ -115,13 +117,14 @@ public static bool IsJsonResponse(this HttpResponseMessage response) var json = await response.ReadAsIIIFJsonAsync(ensureSuccess, settings ?? new JsonSerializerSettings()); return json; } - + private static async Task ReadWithContext( this HttpResponseMessage response, bool ensureSuccess, - JsonSerializerSettings? settings) + JsonSerializerSettings? settings) where T : new() { - var json = await response.ReadAsPresentationJsonAsync(ensureSuccess, settings ?? new JsonSerializerSettings()); + var json = await response.ReadAsPresentationJsonAsync(ensureSuccess, + settings ?? new JsonSerializerSettings(IIIFSerialiserX.DeserializerSettings)); return json; } } \ No newline at end of file diff --git a/src/IIIFPresentation/Models/API/Collection/FlatCollection.cs b/src/IIIFPresentation/Models/API/Collection/FlatCollection.cs index 1dc1bd4..0f905fb 100644 --- a/src/IIIFPresentation/Models/API/Collection/FlatCollection.cs +++ b/src/IIIFPresentation/Models/API/Collection/FlatCollection.cs @@ -1,48 +1,35 @@ -using IIIF.Presentation.V3.Strings; -using Newtonsoft.Json; +using Newtonsoft.Json; namespace Models.API.Collection; -public class FlatCollection +public class FlatCollection : IIIF.Presentation.V3.Collection { [JsonProperty("@context")] - public List? Context { get; set; } - - public string? Id { get; set; } - - public string? PublicId { get; set; } - - public PresentationType Type { get; set; } + public new List? Context + { + get => base.Context as List; + set => base.Context = value; + } - public List Behavior { get; set; } = new (); + public string? PublicId { get; set; } - public LanguageMap? Label { get; set; } + [JsonRequired] public string? Slug { get; set; } - public required string Slug { get; set; } - public string? Parent { get; set; } - + public int? ItemsOrder { get; set; } - - public List? Items { get; set; } - - public List? PartOf { get; set; } - + public int TotalItems { get; set; } public View? View { get; set; } - - public List? SeeAlso { get; set; } - + public DateTime Created { get; set; } - + public DateTime Modified { get; set; } - + public string? CreatedBy { get; set; } - + public string? ModifiedBy { get; set; } - + public string? Tags { get; set; } - - public string? Thumbnail { get; set; } } \ No newline at end of file diff --git a/src/IIIFPresentation/Models/API/Collection/PartOf.cs b/src/IIIFPresentation/Models/API/Collection/PartOf.cs index 8c3e53a..ff5da5a 100644 --- a/src/IIIFPresentation/Models/API/Collection/PartOf.cs +++ b/src/IIIFPresentation/Models/API/Collection/PartOf.cs @@ -1,12 +1,13 @@ -using IIIF.Presentation.V3.Strings; +using IIIF.Presentation.V3; +using IIIF.Presentation.V3.Strings; namespace Models.API.Collection; -public class PartOf +public class PartOf(string type) : ResourceBase { - public required string Id { get; set; } + #region Overrides of ResourceBase - public PresentationType Type { get; set; } - - public LanguageMap? Label { get; set; } + public override string Type { get; } = type; + + #endregion } \ No newline at end of file diff --git a/src/IIIFPresentation/Models/API/Collection/SeeAlso.cs b/src/IIIFPresentation/Models/API/Collection/SeeAlso.cs deleted file mode 100644 index 596637e..0000000 --- a/src/IIIFPresentation/Models/API/Collection/SeeAlso.cs +++ /dev/null @@ -1,14 +0,0 @@ -using IIIF.Presentation.V3.Strings; - -namespace Models.API.Collection; - -public class SeeAlso -{ - public required string Id { get; set; } - - public PresentationType Type { get; set; } - - public LanguageMap? Label { get; set; } - - public List? Profile { get; set; } -} \ No newline at end of file diff --git a/src/IIIFPresentation/Models/API/Collection/View.cs b/src/IIIFPresentation/Models/API/Collection/View.cs index edf6e92..00ca10c 100644 --- a/src/IIIFPresentation/Models/API/Collection/View.cs +++ b/src/IIIFPresentation/Models/API/Collection/View.cs @@ -1,13 +1,15 @@ -using System.Text.Json.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace Models.API.Collection; public class View { - [JsonPropertyName("@id")] + [JsonProperty("@id")] public required string Id { get; set; } - [JsonPropertyName("@type")] + [JsonProperty("@type")] + [JsonConverter(typeof(StringEnumConverter))] public PresentationType Type { get; set; } public int Page { get; set; } diff --git a/src/IIIFPresentation/Models/Models.csproj b/src/IIIFPresentation/Models/Models.csproj index 1cae422..15464b0 100644 --- a/src/IIIFPresentation/Models/Models.csproj +++ b/src/IIIFPresentation/Models/Models.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/IIIFPresentation/Repository/Repository.csproj b/src/IIIFPresentation/Repository/Repository.csproj index 13426bb..3627d4b 100644 --- a/src/IIIFPresentation/Repository/Repository.csproj +++ b/src/IIIFPresentation/Repository/Repository.csproj @@ -10,7 +10,7 @@ - +