Skip to content

Commit

Permalink
Merge pull request #54 from digirati-co-uk/feature/external_service
Browse files Browse the repository at this point in the history
Add ExternalService for unknown Service elements
  • Loading branch information
donaldgray authored Nov 19, 2024
2 parents 2d0bef3 + d75535f commit 1a02cf1
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 30 deletions.
18 changes: 18 additions & 0 deletions src/IIIF/IIIF.Tests/Serialisation/ManifestSerialisationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,24 @@ public void CanDeserialiseSerialisedManifest()

deserialised.Should().BeEquivalentTo(sampleManifest);
}

[Fact]
public void CanDeserialiseUnknownServices()
{
var serialisedManifest = "{\"@context\": [\"http://iiif.io/api/presentation/3/context.json\"],\"id\": \"https://iiif.example/12345\",\"type\": \"Manifest\",\"services\": [{\"id\": \"https://iiif.example.org/1234#tracking\",\"type\": \"Text\",\"profile\": \"http://universalviewer.io/tracking-extensions-profile\",\"label\": {\"en\": [\"Format: Monograph, Institution: n/a, foobarbaz\"]}}]}";
var expectedServices = new List<ExternalService>
{
new ExternalService("Text")
{
Id = "https://iiif.example.org/1234#tracking",
Profile = "http://universalviewer.io/tracking-extensions-profile",
Label = new LanguageMap("en", "Format: Monograph, Institution: n/a, foobarbaz"),
}
};

var deserialised = serialisedManifest.FromJson<Manifest>();
deserialised.Services.Should().BeEquivalentTo(expectedServices);
}

[Fact]
public void CanDeserialiseSerialisedManifest_Stream()
Expand Down
137 changes: 137 additions & 0 deletions src/IIIF/IIIF.Tests/Serialisation/ServiceConverterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using System;
using IIIF.ImageApi.V2;
using IIIF.ImageApi.V3;
using IIIF.Serialisation.Deserialisation;
using Newtonsoft.Json;

namespace IIIF.Tests.Serialisation;

public class ServiceConverterTests
{
private readonly ServiceConverter sut = new();

[Theory]
[InlineData("SearchService1", typeof(IIIF.Search.V1.SearchService))]
[InlineData("AutoCompleteService1", typeof(IIIF.Search.V1.AutoCompleteService))]
public void ReadJson_KnownSearch1Services_FromType(string type, Type expected)
{
var jsonId = $"{{\"@type\": \"{type}\"}}";

var result = JsonConvert.DeserializeObject<IService>(jsonId, sut);

result.Should().BeOfType(expected);
}

[Theory]
[InlineData("AuthLogoutService1", typeof(IIIF.Auth.V1.AuthLogoutService))]
[InlineData("AuthTokenService1", typeof(IIIF.Auth.V1.AuthTokenService))]
public void ReadJson_KnownAuth1Services_FromType(string type, Type expected)
{
var jsonId = $"{{\"@type\": \"{type}\"}}";

var result = JsonConvert.DeserializeObject<IService>(jsonId, sut);

result.Should().BeOfType(expected);
}

[Theory]
[InlineData("iiif:Image", "'iiif:Image' is type for presentation 2")]
[InlineData("ImageService2", "'ImageService2' is type when rendered on presentation 3")]
public void ReadJson_ImageService2_FromType(string type, string because)
{
var jsonId = $"{{\"@type\": \"{type}\"}}";

var result = JsonConvert.DeserializeObject<IService>(jsonId, sut);

result.Should().BeOfType<ImageService2>(because);
}

[Theory]
[InlineData("AuthAccessService2", typeof(IIIF.Auth.V2.AuthAccessService2))]
[InlineData("AuthAccessTokenService2", typeof(IIIF.Auth.V2.AuthAccessTokenService2))]
[InlineData("AuthLogoutService2", typeof(IIIF.Auth.V2.AuthLogoutService2))]
[InlineData("AuthProbeService2", typeof(IIIF.Auth.V2.AuthProbeService2))]
public void ReadJson_KnownAuth2Services_FromType(string type, Type expected)
{
var jsonId = $"{{\"type\": \"{type}\"}}";

var result = JsonConvert.DeserializeObject<IService>(jsonId, sut);

result.Should().BeOfType(expected);
}

[Fact]
public void ReadJson_ImageService3_FromType()
{
var jsonId = "{\"type\": \"ImageService3\"}";

var result = JsonConvert.DeserializeObject<IService>(jsonId, sut);

result.Should().BeOfType<ImageService3>();
}

[Theory]
[InlineData(IIIF.Auth.V1.AuthLogoutService.AuthLogout1Profile, typeof(IIIF.Auth.V1.AuthLogoutService))]
[InlineData(IIIF.Auth.V1.AuthTokenService.AuthToken1Profile, typeof(IIIF.Auth.V1.AuthTokenService))]
[InlineData(IIIF.Auth.V0.AuthLogoutService.AuthLogout0Profile, typeof(IIIF.Auth.V0.AuthLogoutService))]
[InlineData(IIIF.Auth.V0.AuthTokenService.AuthToken0Profile, typeof(IIIF.Auth.V0.AuthTokenService))]
[InlineData("http://iiif.io/api/auth/0/login", typeof(IIIF.Auth.V0.AuthCookieService))]
[InlineData("http://iiif.io/api/auth/0/clickthrough", typeof(IIIF.Auth.V0.AuthCookieService))]
[InlineData("http://iiif.io/api/auth/0/kiosk", typeof(IIIF.Auth.V0.AuthCookieService))]
[InlineData("http://iiif.io/api/auth/0/external", typeof(IIIF.Auth.V0.AuthCookieService))]
[InlineData("http://iiif.io/api/auth/1/login", typeof(IIIF.Auth.V1.AuthCookieService))]
[InlineData("http://iiif.io/api/auth/1/clickthrough", typeof(IIIF.Auth.V1.AuthCookieService))]
[InlineData("http://iiif.io/api/auth/1/kiosk", typeof(IIIF.Auth.V1.AuthCookieService))]
[InlineData("http://iiif.io/api/auth/1/external", typeof(IIIF.Auth.V1.AuthCookieService))]
public void ReadJson_KnownAuthServices_FromProfile(string profile, Type expected)
{
var jsonId = $"{{\"profile\": \"{profile}\"}}";

var result = JsonConvert.DeserializeObject<IService>(jsonId, sut);

result.Should().BeOfType(expected);
}

[Theory]
[InlineData(IIIF.Search.V2.AutoCompleteService.AutoComplete2Profile, typeof(IIIF.Search.V2.AutoCompleteService))]
[InlineData(IIIF.Search.V1.AutoCompleteService.AutoCompleteService1Profile, typeof(IIIF.Search.V1.AutoCompleteService))]
[InlineData(IIIF.Search.V2.SearchService.Search2Profile, typeof(IIIF.Search.V2.SearchService))]
public void ReadJson_KnownSearchServices_FromProfile(string profile, Type expected)
{
var jsonId = $"{{\"profile\": \"{profile}\"}}";

var result = JsonConvert.DeserializeObject<IService>(jsonId, sut);

result.Should().BeOfType(expected);
}

[Fact]
public void ReadJson_V2ServiceReference_IfTypeAndIdOnly()
{
var jsonId = "{\"@type\": \"AuthCookieService1\", \"@id\": \"https://service-reference-test\" }";

var result = JsonConvert.DeserializeObject<IService>(jsonId, sut);

result.Should().BeOfType<V2ServiceReference>();
}

[Fact]
public void ReadJson_FallsBackTo_V2ExternalService_IfAtType_AndUnableToDetermine()
{
var jsonId = "{\"@type\": \"Text\", \"@id\": \"https://service-reference-test\", \"label\": \"test\" }";

var result = JsonConvert.DeserializeObject<IService>(jsonId, sut);

result.Should().BeOfType<IIIF.Presentation.V2.ExternalService>();
}

[Fact]
public void ReadJson_FallsBackTo_V3ExternalService_IfType_AndUnableToDetermine()
{
var jsonId = "{\"type\": \"Text\", \"id\": \"https://service-reference-test\", \"label\": { \"none\": [\"test\"]} }";

var result = JsonConvert.DeserializeObject<IService>(jsonId, sut);

result.Should().BeOfType<IIIF.Presentation.V3.ExternalService>();
}
}
10 changes: 10 additions & 0 deletions src/IIIF/IIIF/Presentation/V2/ExternalService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace IIIF.Presentation.V2;

/// <summary>
/// Represents a generic, unknown <see cref="IService"/> reference
/// </summary>
public class ExternalService : ResourceBase, IService
{
[JsonProperty(PropertyName = "@type", Order = 3)]
public override string? Type { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Newtonsoft.Json;

namespace IIIF.Presentation.V3.Annotation;
namespace IIIF.Presentation.V3.Annotation;

public class PaintingAnnotation : Annotation
{
Expand Down
14 changes: 14 additions & 0 deletions src/IIIF/IIIF/Presentation/V3/ExternalService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace IIIF.Presentation.V3;

/// <summary>
/// Represents a generic, unknown <see cref="IService"/> reference
/// </summary>
public class ExternalService : ResourceBase, IService
{
public override string Type { get; }

public ExternalService(string type)
{
Type = type;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using System;
using IIIF.Auth.V2;
using IIIF.Presentation.V3.Content;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace IIIF.Serialisation.Deserialisation;
Expand Down
62 changes: 37 additions & 25 deletions src/IIIF/IIIF/Serialisation/Deserialisation/ServiceConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class ServiceConverter : ReadOnlyConverter<IService>
{
IService? service = null;
var atType = jsonObject["@type"];
var type = jsonObject["type"];
if (atType != null)
service = atType.Value<string>() switch
{
Expand All @@ -34,27 +35,28 @@ public class ServiceConverter : ReadOnlyConverter<IService>
"AuthTokenService1" => new Auth.V1.AuthTokenService(),
"AutoCompleteService1" => new Search.V1.AutoCompleteService(),
nameof(ImageService2) => new ImageService2(),
"iiif:Image" => new ImageService2(),
_ => null,
};
if (service != null) return service;

if (type != null)
service = type.Value<string>() switch
{
nameof(ImageService3) => new ImageService3(),
nameof(AuthAccessService2) => new AuthAccessService2(),
nameof(AuthAccessTokenService2) => new AuthAccessTokenService2(),
nameof(AuthLogoutService2) => new AuthLogoutService2(),
nameof(AuthProbeService2) => new AuthProbeService2(),
_ => null
};
if (service != null) return service;

if (service == null)
{
var type = jsonObject["type"];
if (type != null)
service = type.Value<string>() switch
{
nameof(ImageService3) => new ImageService3(),
nameof(AuthAccessService2) => new AuthAccessService2(),
nameof(AuthAccessTokenService2) => new AuthAccessTokenService2(),
nameof(AuthLogoutService2) => new AuthLogoutService2(),
nameof(AuthProbeService2) => new AuthProbeService2(),
_ => null
};
}

if (service == null)
var profileToken = jsonObject["profile"];
if (profileToken != null)
{
var profile = jsonObject["profile"].Value<string>();
var profile = profileToken.Value<string>();
service = profile switch
{
Auth.V1.AuthLogoutService.AuthLogout1Profile => new Auth.V1.AuthLogoutService(),
Expand All @@ -66,21 +68,31 @@ public class ServiceConverter : ReadOnlyConverter<IService>
Search.V2.SearchService.Search2Profile => new Search.V2.SearchService(),
_ => null
};
if (service != null) return service;

if (service == null)
{
const string auth0 = "http://iiif.io/api/auth/0/";
const string auth1 = "http://iiif.io/api/auth/1/";

if (profile.StartsWith(auth0))
service = new Auth.V0.AuthCookieService(profile);
else if (profile.StartsWith(auth1)) service = new Auth.V1.AuthCookieService(profile);
}
const string auth0 = "http://iiif.io/api/auth/0/";
const string auth1 = "http://iiif.io/api/auth/1/";

if (profile.StartsWith(auth0)) return new Auth.V0.AuthCookieService(profile);
if (profile.StartsWith(auth1)) return new Auth.V1.AuthCookieService(profile);
}

// TODO handle ResourceBase items

if (service == null) service = new V2ServiceReference();
if (atType != null)
{
// if there's @id and @type only, service reference
if (jsonObject.Count == 2 && jsonObject["@id"] != null)
return new V2ServiceReference();
else
return new Presentation.V2.ExternalService();
}

if (type != null)
{
return new Presentation.V3.ExternalService(type.Value<string>());
}

return service;
}
Expand Down

0 comments on commit 1a02cf1

Please sign in to comment.