Skip to content

Commit

Permalink
Merge pull request #51 from digirati-co-uk/cookbook-recipe-tests
Browse files Browse the repository at this point in the history
Make iiif-net parse all the Manifests in the Cookbook
  • Loading branch information
donaldgray authored Oct 28, 2024
2 parents 2f29b16 + 1f41564 commit 2d0bef3
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 15 deletions.
17 changes: 17 additions & 0 deletions src/IIIF/IIIF.Tests/Serialisation/CookbookDeserialization.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using IIIF.Presentation.V3;
using IIIF.Tests.Serialisation.Data;

namespace IIIF.Tests.Serialisation;

[Trait("Category", "Cookbook")]
public class CookbookDeserialization
{
[Theory]
[ClassData(typeof(CookbookManifestData))]
public void Can_Deserialize_Cookbook_Manifest(string manifestId, Manifest manifest)
{
// perfunctory assertion
manifest.Should().NotBeNull($"{manifestId} is a valid cookbook manifest");
manifest.Id.Should().Be(manifestId);
}
}
64 changes: 64 additions & 0 deletions src/IIIF/IIIF.Tests/Serialisation/Data/CookbookManifestData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.Http;
using IIIF.Presentation.V3;
using IIIF.Serialisation;

namespace IIIF.Tests.Serialisation.Data;

/// <summary>
/// Used as [ClassData] - contains Manifests from IIIF Cookbook to validate deserialisation
/// </summary>
public class CookbookManifestData : IEnumerable<object[]>
{
// This will store { manifest-id, deserialized-manifest }
private readonly List<object[]> data = new();

// these have bugs in the cookbook, see https://github.com/IIIF/cookbook-recipes/pull/546
private List<string> skip = new()
{
"https://iiif.io/api/cookbook/recipe/0219-using-caption-file/manifest.json",
"https://iiif.io/api/cookbook/recipe/0040-image-rotation-service/manifest-service.json"
};

public CookbookManifestData()
{
using var httpClient = new HttpClient();
var theseusCollection =
GetIIIFResource<Collection>("https://theseus-viewer.netlify.app/cookbook-collection.json", true);

foreach (var item in theseusCollection.Items!)
{
if (item is Manifest manifestRef)
{
if (skip.Contains(manifestRef.Id)) continue;

var iiif = GetIIIFResource<Manifest>(manifestRef.Id);
data.Add(new object[] { manifestRef.Id, iiif });
}
}

T GetIIIFResource<T>(string url, bool mustSucceed = false) where T : JsonLdBase
{
var resource = httpClient.GetAsync(url).Result;
if (mustSucceed) resource.EnsureSuccessStatusCode();
if (!resource.IsSuccessStatusCode) return null;

try
{
var iiif = resource.Content.ReadAsStream().FromJsonStream<T>();
return iiif;
}
catch (Exception)
{
if (mustSucceed) throw;
return null;
}
}
}

public IEnumerator<object[]> GetEnumerator() => data.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

namespace IIIF.Presentation.V3.Content;

public class Audio : ExternalResource, ITemporal, IPaintable
public class Sound : ExternalResource, ITemporal, IPaintable
{
public double? Duration { get; set; }

public Audio() : base("Sound")
public Sound() : base(nameof(Sound))
{
}
}
7 changes: 7 additions & 0 deletions src/IIIF/IIIF/Presentation/V3/Selectors/SvgSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace IIIF.Presentation.V3.Selectors;

public class SvgSelector : ISelector
{
public string? Type => nameof(SvgSelector);
public string? Value { get; set; }
}
10 changes: 6 additions & 4 deletions src/IIIF/IIIF/Presentation/V3/SpecificResource.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
using IIIF.Presentation.V3.Selectors;
using Newtonsoft.Json;
using IIIF.Presentation.V3.Annotation;
using IIIF.Presentation.V3.Selectors;
using IIIF.Serialisation;

namespace IIIF.Presentation.V3;

public class SpecificResource : ResourceBase, IStructuralLocation
public class SpecificResource : ResourceBase, IStructuralLocation, IPaintable
{
public override string Type => nameof(SpecificResource);

[JsonProperty(Order = 101)] public string Source { get; set; }
[JsonConverter(typeof(SourceConverter))]
[JsonProperty(Order = 101)] public IPaintable Source { get; set; }

[JsonProperty(Order = 102)] public ISelector Selector { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class ExternalResourceConverter : ReadOnlyConverter<ExternalResource>
var type = jsonObject["type"].Value<string>();
var externalResource = type switch
{
nameof(Audio) => new Audio(),
nameof(Sound) => new Sound(),
nameof(Video) => new Video(),
nameof(Image) => new Image(),
_ => new ExternalResource(type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ public class PaintableConverter : ReadOnlyConverter<IPaintable>

IPaintable? paintable = jsonObject["type"].Value<string>() switch
{
nameof(Audio) => new Audio(),
nameof(Sound) => new Sound(),
nameof(Video) => new Video(),
nameof(Image) => new Image(),
nameof(Canvas) => new Canvas(),
"Choice" => new PaintingChoice(),
nameof(SpecificResource) => new SpecificResource(),
_ => null
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class ResourceBaseV3Converter : ReadOnlyConverter<ResourceBase>
nameof(Annotation) => new Annotation(),
nameof(AnnotationCollection) => new AnnotationCollection(),
nameof(AnnotationPage) => new AnnotationPage(),
nameof(Audio) => new Audio(),
nameof(Sound) => new Sound(),
nameof(Canvas) => new Canvas(),
nameof(Collection) => new Collection(),
nameof(Image) => new Image(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public class ResourceConverter : ReadOnlyConverter<IResource>
nameof(AuthAccessTokenService2) => new AuthAccessTokenService2(),
nameof(AuthLogoutService2) => new AuthLogoutService2(),
nameof(AuthProbeService2) => new AuthProbeService2(),
nameof(Audio) => new Audio(),
nameof(Sound) => new Sound(),
nameof(Video) => new Video(),
nameof(Image) => new Image(),
_ => null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ public class SelectorConverter : ReadOnlyConverter<ISelector>
{
nameof(AudioContentSelector) => new AudioContentSelector(),
nameof(ImageApiSelector) => new ImageApiSelector(),
"iiif:ImageApiSelector" => new ImageApiSelector(),
nameof(PointSelector) => new PointSelector(),
nameof(VideoContentSelector) => new VideoContentSelector()
nameof(VideoContentSelector) => new VideoContentSelector(),
nameof(SvgSelector) => new SvgSelector()
};

serializer.Populate(jsonObject.CreateReader(), selector);
Expand Down
57 changes: 57 additions & 0 deletions src/IIIF/IIIF/Serialisation/SourceConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using IIIF.Presentation.V3;
using IIIF.Presentation.V3.Annotation;
using IIIF.Presentation.V3.Content;
using IIIF.Utils;
using Newtonsoft.Json.Linq;

namespace IIIF.Serialisation;

public class SourceConverter : JsonConverter<IPaintable>
{
public override IPaintable? ReadJson(JsonReader reader, Type objectType, IPaintable? existingValue, bool hasExistingValue,
JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.String)
{
// We do not know that this is a Canvas...
// We would need knowledge of the rest of the IIIF Resource
return new Canvas { Id = reader.Value.ToString() };
}
else if (reader.TokenType == JsonToken.StartObject)
{
var obj = JObject.Load(reader);
var type = obj["type"].Value<string>();
IPaintable paintable = type switch
{
nameof(Sound) => new Sound(),
nameof(Video) => new Video(),
nameof(Image) => new Image(),
nameof(Canvas) => new Canvas(),
nameof(SpecificResource) => new SpecificResource()
};
serializer.Populate(obj.CreateReader(), paintable);
return paintable;
}

return null;
}

public override void WriteJson(JsonWriter writer, IPaintable? value, JsonSerializer serializer)
{
if (value is Canvas canvas && (canvas.SerialiseTargetAsId || IsSimpleCanvas(canvas)))
{
writer.WriteValue(canvas.Id);
return;
}

// Default, pass through behaviour:
JObject.FromObject(value, serializer).WriteTo(writer);
}

private static bool IsSimpleCanvas(Canvas canvas)
{
return canvas.Width == null && canvas.Duration == null && canvas.Items.IsNullOrEmpty();
}

}
10 changes: 6 additions & 4 deletions src/IIIF/IIIF/Serialisation/TargetConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ public class TargetConverter : JsonConverter<IStructuralLocation>
var obj = JObject.Load(reader);

var type = obj["type"].Value<string>();
return type switch
IStructuralLocation structuralLocation = type switch
{
nameof(Canvas) => obj.ToObject<Canvas>(),
nameof(Range) => obj.ToObject<Range>(),
nameof(SpecificResource) => obj.ToObject<SpecificResource>()
nameof(Canvas) => new Canvas(),
nameof(Range) => new Range(),
nameof(SpecificResource) => new SpecificResource()
};
serializer.Populate(obj.CreateReader(), structuralLocation);
return structuralLocation;
}

return null;
Expand Down

0 comments on commit 2d0bef3

Please sign in to comment.