diff --git a/src/IIIF/IIIF.Tests/ContextHelperTests.cs b/src/IIIF/IIIF.Tests/ContextHelperTests.cs new file mode 100644 index 0000000..15f7d4c --- /dev/null +++ b/src/IIIF/IIIF.Tests/ContextHelperTests.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; + +namespace IIIF.Tests; + +public class ContextHelperTests +{ + [Fact] + public void EnsureContext_AddsContext_IfNoExisting() + { + // Arrange + var jsonLdBase = new TestJsonLdBase(); + const string customContext = "http://my-custom-context"; + + // Act + jsonLdBase.EnsureContext(customContext); + + // Assert + jsonLdBase.Context.Should().BeOfType() + .And.Subject.Should().Be(customContext); + } + + [Fact] + public void EnsureContext_AddsContext_IfOneExisting_WithoutReordering() + { + // Arrange + const string context = "http://existing-context"; + const string customContext = "http://my-custom-context"; + var jsonLdBase = new TestJsonLdBase { Context = context }; + + var expected = new List { context, customContext }; + + // Act + jsonLdBase.EnsureContext(customContext); + + // Assert + (jsonLdBase.Context as List).Should().ContainInOrder(expected); + } + + [Theory] + [InlineData(IIIF.Presentation.Context.Presentation2Context)] + [InlineData(IIIF.Presentation.Context.Presentation3Context)] + [InlineData(IIIF.ImageApi.V2.ImageService2.Image2Context)] + [InlineData(IIIF.ImageApi.V3.ImageService3.Image3Context)] + [InlineData("http://my-custom-context")] + public void EnsureContext_NoOp_IfContextAlreadyExistsAsSingle(string context) + { + // Arrange + var jsonLdBase = new TestJsonLdBase { Context = context }; + + // Act + jsonLdBase.EnsureContext(context); + + // Assert + jsonLdBase.Context.Should().BeOfType() + .And.Subject.Should().Be(context); + } + + [Theory] + [InlineData(IIIF.Presentation.Context.Presentation2Context)] + [InlineData(IIIF.Presentation.Context.Presentation3Context)] + [InlineData(IIIF.ImageApi.V2.ImageService2.Image2Context)] + [InlineData(IIIF.ImageApi.V3.ImageService3.Image3Context)] + [InlineData("http://my-custom-context")] + public void EnsureContext_NoOp_IfContextAlreadyExistsInList(string context) + { + // Arrange + const string customContext = "http://existing-context"; + var jsonLdBase = new TestJsonLdBase { Context = customContext }; + jsonLdBase.EnsureContext(context); + + var expected = new List { customContext, context }; + + // Act + jsonLdBase.EnsureContext(customContext); + + // Assert + (jsonLdBase.Context as List).Should().ContainInOrder(expected); + } + + [Theory] + [InlineData(IIIF.Presentation.Context.Presentation2Context)] + [InlineData(IIIF.Presentation.Context.Presentation3Context)] + [InlineData(IIIF.ImageApi.V2.ImageService2.Image2Context)] + [InlineData(IIIF.ImageApi.V3.ImageService3.Image3Context)] + public void EnsureContext_AlwaysReordersIIIFContextsToLast_IfOthersAddedAfter(string iiifContext) + { + // Arrange + const string customContext = "http://my-custom-context"; + var jsonLdBase = new TestJsonLdBase { Context = iiifContext }; + + var expected = new List { customContext, iiifContext }; + + // Act + jsonLdBase.EnsureContext(customContext); + + // Assert + (jsonLdBase.Context as List).Should().ContainInOrder(expected); + } + + [Theory] + [InlineData(IIIF.Presentation.Context.Presentation2Context)] + [InlineData(IIIF.Presentation.Context.Presentation3Context)] + [InlineData(IIIF.ImageApi.V2.ImageService2.Image2Context)] + [InlineData(IIIF.ImageApi.V3.ImageService3.Image3Context)] + public void EnsureContext_AlwaysReordersIIIFContextsToLast_IfMultipleAdds(string iiifContext) + { + // Arrange + const string context = "http://existing-context"; + const string customContext = "http://my-custom-context"; + var jsonLdBase = new TestJsonLdBase { Context = context }; + + var expected = new List { context, customContext, iiifContext }; + + // Act + jsonLdBase.EnsureContext(iiifContext); + jsonLdBase.EnsureContext(customContext); + + // Assert + (jsonLdBase.Context as List).Should().ContainInOrder(expected); + } + + [Theory] + [MemberData(nameof(SampleContexts))] + public void EnsureContext_Throws_IfMultipleIIIFContextsAdded(string first, string second) + { + // Arrange + var jsonLdBase = new TestJsonLdBase { Context = first }; + + // Act + Action action = () => jsonLdBase.EnsureContext(second); + + // Assert + action.Should().Throw(); + } + + public static IEnumerable SampleContexts => + new List + { + new object[] { IIIF.Presentation.Context.Presentation2Context, IIIF.Presentation.Context.Presentation3Context }, + new object[] { IIIF.ImageApi.V2.ImageService2.Image2Context, IIIF.ImageApi.V3.ImageService3.Image3Context }, + new object[] { IIIF.Presentation.Context.Presentation2Context, IIIF.ImageApi.V3.ImageService3.Image3Context }, + new object[] { IIIF.ImageApi.V3.ImageService3.Image3Context , IIIF.Presentation.Context.Presentation3Context }, + }; + + private class TestJsonLdBase : JsonLdBase + { + } +} \ No newline at end of file diff --git a/src/IIIF/IIIF.Tests/Usings.cs b/src/IIIF/IIIF.Tests/Usings.cs new file mode 100644 index 0000000..a9ca8da --- /dev/null +++ b/src/IIIF/IIIF.Tests/Usings.cs @@ -0,0 +1,2 @@ +global using FluentAssertions; +global using Xunit; \ No newline at end of file diff --git a/src/IIIF/IIIF/ContextHelper.cs b/src/IIIF/IIIF/ContextHelper.cs new file mode 100644 index 0000000..5413052 --- /dev/null +++ b/src/IIIF/IIIF/ContextHelper.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace IIIF; + +public static class ContextHelper +{ + // This is a list of known IIIF contexts that must be last in Contexts list + // Only 1 can be present in any given Context list + private static List knownContexts = new() + { + Presentation.Context.Presentation2Context, + Presentation.Context.Presentation3Context, + ImageApi.V2.ImageService2.Image2Context, + ImageApi.V3.ImageService3.Image3Context, + }; + + /// + /// Adds specified Context to list. + /// The IIIF context must be last in the list, to override any that come before it. + /// + public static void EnsureContext(this JsonLdBase resource, string contextToEnsure) + { + if (resource.Context == null) + { + resource.Context = contextToEnsure; + return; + } + + var workingContexts = GetWorkingContexts(resource); + if (workingContexts.Contains(contextToEnsure)) + { + // Context already added - no-op + SetContext(resource, workingContexts); + return; + } + workingContexts.Add(contextToEnsure); + + if (workingContexts.Intersect(knownContexts).Count() > 1) + { + throw new InvalidOperationException( + "You cannot have multiple IIIF contexts (Presentation or Image) in the same resource."); + } + + var iiifContext = workingContexts.FirstOrDefault(wc => knownContexts.Contains(wc)); + + // If we have an IIIF context it must be last in list + if (!string.IsNullOrEmpty(iiifContext) && workingContexts.Count > 1) + { + workingContexts.Remove(iiifContext); + workingContexts.Add(iiifContext); + } + + // Now JSON-LD rules. The @context is the only Presentation 3 element that has this. + SetContext(resource, workingContexts); + } + + private static void SetContext(JsonLdBase resource, IReadOnlyList workingContexts) + => resource.Context = workingContexts.Count == 1 ? workingContexts[0] : workingContexts; + + private static List GetWorkingContexts(JsonLdBase resource) + { + if (resource.Context is List existingContexts) return existingContexts; + + if (resource.Context is string singleContext) return new List { singleContext }; + + return new List(1); + } +} \ No newline at end of file diff --git a/src/IIIF/IIIF/Presentation/Context.cs b/src/IIIF/IIIF/Presentation/Context.cs index c394e52..a1f89c3 100644 --- a/src/IIIF/IIIF/Presentation/Context.cs +++ b/src/IIIF/IIIF/Presentation/Context.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace IIIF.Presentation; +namespace IIIF.Presentation; /// /// Contains JSON-LD Contexts for IIIF Presentation API. @@ -27,55 +24,4 @@ public static void EnsurePresentation2Context(this JsonLdBase resource) { resource.EnsureContext(Presentation2Context); } - - // The IIIF context must be last in the list, to override any that come before it. - public static void EnsureContext(this JsonLdBase resource, string contextToEnsure) - { - if (resource.Context == null) - { - resource.Context = contextToEnsure; - return; - } - - List workingContexts = new(); - if (resource.Context is List existingContexts) workingContexts = existingContexts; - - if (resource.Context is string singleContext) workingContexts = new List { singleContext }; - - List newContexts = new(); - var requiresPresentation3Context = contextToEnsure == Presentation3Context; - var requiresPresentation2Context = contextToEnsure == Presentation2Context; - foreach (var workingContext in workingContexts) - switch (workingContext) - { - case Presentation3Context: - requiresPresentation3Context = true; - break; - case Presentation2Context: - requiresPresentation2Context = true; - break; - default: - newContexts.Add(workingContext); - break; - } - - // Add the new context to the list but not if it supposed to come last - if (!newContexts.Contains(contextToEnsure) - && contextToEnsure != Presentation3Context - && contextToEnsure != Presentation2Context) - newContexts.Add(contextToEnsure); - - if (requiresPresentation2Context && requiresPresentation3Context) - throw new InvalidOperationException( - "You cannot have Presentation 2 and Presentation 3 contexts in the same resource."); - // These have to come last - if (requiresPresentation3Context) newContexts.Add(Presentation3Context); - if (requiresPresentation2Context) newContexts.Add(Presentation2Context); - - // Now JSON-LD rules. The @context is the only Presentation 3 element that has this. - if (newContexts.Count == 1) - resource.Context = newContexts[0]; - else - resource.Context = newContexts; - } } \ No newline at end of file