From 45bdd0f154a71603621fb4ea40bec7e34d289620 Mon Sep 17 00:00:00 2001 From: Donald Gray Date: Mon, 18 Nov 2024 19:42:13 +0000 Subject: [PATCH 1/2] Add ExternalService for unknown Service elements --- .../ManifestSerialisationTests.cs | 18 +++++ .../IIIF/Presentation/V2/ExternalService.cs | 10 +++ .../V3/Annotation/PaintingAnnotation.cs | 4 +- .../IIIF/Presentation/V3/ExternalService.cs | 14 ++++ .../ExternalResourceConverter.cs | 2 - .../Deserialisation/ServiceConverter.cs | 81 ++++++++++--------- 6 files changed, 87 insertions(+), 42 deletions(-) create mode 100644 src/IIIF/IIIF/Presentation/V2/ExternalService.cs create mode 100644 src/IIIF/IIIF/Presentation/V3/ExternalService.cs diff --git a/src/IIIF/IIIF.Tests/Serialisation/ManifestSerialisationTests.cs b/src/IIIF/IIIF.Tests/Serialisation/ManifestSerialisationTests.cs index 4823d0a..1780464 100644 --- a/src/IIIF/IIIF.Tests/Serialisation/ManifestSerialisationTests.cs +++ b/src/IIIF/IIIF.Tests/Serialisation/ManifestSerialisationTests.cs @@ -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 + { + 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(); + deserialised.Services.Should().BeEquivalentTo(expectedServices); + } [Fact] public void CanDeserialiseSerialisedManifest_Stream() diff --git a/src/IIIF/IIIF/Presentation/V2/ExternalService.cs b/src/IIIF/IIIF/Presentation/V2/ExternalService.cs new file mode 100644 index 0000000..c524170 --- /dev/null +++ b/src/IIIF/IIIF/Presentation/V2/ExternalService.cs @@ -0,0 +1,10 @@ +namespace IIIF.Presentation.V2; + +/// +/// Represents a generic, unknown reference +/// +public class ExternalService : ResourceBase, IService +{ + [JsonProperty(PropertyName = "@type", Order = 3)] + public override string? Type { get; set; } +} \ No newline at end of file diff --git a/src/IIIF/IIIF/Presentation/V3/Annotation/PaintingAnnotation.cs b/src/IIIF/IIIF/Presentation/V3/Annotation/PaintingAnnotation.cs index 95b2a61..5a71a4c 100644 --- a/src/IIIF/IIIF/Presentation/V3/Annotation/PaintingAnnotation.cs +++ b/src/IIIF/IIIF/Presentation/V3/Annotation/PaintingAnnotation.cs @@ -1,6 +1,4 @@ -using Newtonsoft.Json; - -namespace IIIF.Presentation.V3.Annotation; +namespace IIIF.Presentation.V3.Annotation; public class PaintingAnnotation : Annotation { diff --git a/src/IIIF/IIIF/Presentation/V3/ExternalService.cs b/src/IIIF/IIIF/Presentation/V3/ExternalService.cs new file mode 100644 index 0000000..0d82d84 --- /dev/null +++ b/src/IIIF/IIIF/Presentation/V3/ExternalService.cs @@ -0,0 +1,14 @@ +namespace IIIF.Presentation.V3; + +/// +/// Represents a generic, unknown reference +/// +public class ExternalService : ResourceBase, IService +{ + public override string Type { get; } + + public ExternalService(string type) + { + Type = type; + } +} \ No newline at end of file diff --git a/src/IIIF/IIIF/Serialisation/Deserialisation/ExternalResourceConverter.cs b/src/IIIF/IIIF/Serialisation/Deserialisation/ExternalResourceConverter.cs index dd945c7..2992d7a 100644 --- a/src/IIIF/IIIF/Serialisation/Deserialisation/ExternalResourceConverter.cs +++ b/src/IIIF/IIIF/Serialisation/Deserialisation/ExternalResourceConverter.cs @@ -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; diff --git a/src/IIIF/IIIF/Serialisation/Deserialisation/ServiceConverter.cs b/src/IIIF/IIIF/Serialisation/Deserialisation/ServiceConverter.cs index f38e691..e80dac7 100644 --- a/src/IIIF/IIIF/Serialisation/Deserialisation/ServiceConverter.cs +++ b/src/IIIF/IIIF/Serialisation/Deserialisation/ServiceConverter.cs @@ -26,6 +26,7 @@ public class ServiceConverter : ReadOnlyConverter { IService? service = null; var atType = jsonObject["@type"]; + var type = jsonObject["type"]; if (atType != null) service = atType.Value() switch { @@ -34,53 +35,59 @@ public class ServiceConverter : ReadOnlyConverter "AuthTokenService1" => new Auth.V1.AuthTokenService(), "AutoCompleteService1" => new Search.V1.AutoCompleteService(), nameof(ImageService2) => new ImageService2(), - _ => null + _ => null, }; + if (service != null) return service; - if (service == null) - { - var type = jsonObject["type"]; - if (type != null) - service = type.Value() 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 profile = jsonObject["profile"].Value(); - service = profile switch + if (type != null) + service = type.Value() switch { - Auth.V1.AuthLogoutService.AuthLogout1Profile => new Auth.V1.AuthLogoutService(), - Auth.V1.AuthTokenService.AuthToken1Profile => new Auth.V1.AuthTokenService(), - Auth.V0.AuthLogoutService.AuthLogout0Profile => new Auth.V0.AuthLogoutService(), - Auth.V0.AuthTokenService.AuthToken0Profile => new Auth.V0.AuthTokenService(), - Search.V2.AutoCompleteService.AutoComplete2Profile => new Search.V2.AutoCompleteService(), - Search.V1.AutoCompleteService.AutoCompleteService1Profile => new Search.V1.AutoCompleteService(), - Search.V2.SearchService.Search2Profile => new Search.V2.SearchService(), + 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) - { - 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); - } - } + var profile = jsonObject["profile"].Value(); + service = profile switch + { + Auth.V1.AuthLogoutService.AuthLogout1Profile => new Auth.V1.AuthLogoutService(), + Auth.V1.AuthTokenService.AuthToken1Profile => new Auth.V1.AuthTokenService(), + Auth.V0.AuthLogoutService.AuthLogout0Profile => new Auth.V0.AuthLogoutService(), + Auth.V0.AuthTokenService.AuthToken0Profile => new Auth.V0.AuthTokenService(), + Search.V2.AutoCompleteService.AutoComplete2Profile => new Search.V2.AutoCompleteService(), + Search.V1.AutoCompleteService.AutoCompleteService1Profile => new Search.V1.AutoCompleteService(), + Search.V2.SearchService.Search2Profile => new Search.V2.SearchService(), + _ => null + }; + if (service != null) return service; + + + 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()); + } return service; } From d75535fac7e3448222b323547e8e1e4e4ecdffcb Mon Sep 17 00:00:00 2001 From: Donald Gray Date: Tue, 19 Nov 2024 11:19:53 +0000 Subject: [PATCH 2/2] Test for service converter --- .../Serialisation/ServiceConverterTests.cs | 137 ++++++++++++++++++ .../Deserialisation/ServiceConverter.cs | 37 +++-- 2 files changed, 158 insertions(+), 16 deletions(-) create mode 100644 src/IIIF/IIIF.Tests/Serialisation/ServiceConverterTests.cs diff --git a/src/IIIF/IIIF.Tests/Serialisation/ServiceConverterTests.cs b/src/IIIF/IIIF.Tests/Serialisation/ServiceConverterTests.cs new file mode 100644 index 0000000..e47f8e8 --- /dev/null +++ b/src/IIIF/IIIF.Tests/Serialisation/ServiceConverterTests.cs @@ -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(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(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(jsonId, sut); + + result.Should().BeOfType(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(jsonId, sut); + + result.Should().BeOfType(expected); + } + + [Fact] + public void ReadJson_ImageService3_FromType() + { + var jsonId = "{\"type\": \"ImageService3\"}"; + + var result = JsonConvert.DeserializeObject(jsonId, sut); + + result.Should().BeOfType(); + } + + [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(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(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(jsonId, sut); + + result.Should().BeOfType(); + } + + [Fact] + public void ReadJson_FallsBackTo_V2ExternalService_IfAtType_AndUnableToDetermine() + { + var jsonId = "{\"@type\": \"Text\", \"@id\": \"https://service-reference-test\", \"label\": \"test\" }"; + + var result = JsonConvert.DeserializeObject(jsonId, sut); + + result.Should().BeOfType(); + } + + [Fact] + public void ReadJson_FallsBackTo_V3ExternalService_IfType_AndUnableToDetermine() + { + var jsonId = "{\"type\": \"Text\", \"id\": \"https://service-reference-test\", \"label\": { \"none\": [\"test\"]} }"; + + var result = JsonConvert.DeserializeObject(jsonId, sut); + + result.Should().BeOfType(); + } +} \ No newline at end of file diff --git a/src/IIIF/IIIF/Serialisation/Deserialisation/ServiceConverter.cs b/src/IIIF/IIIF/Serialisation/Deserialisation/ServiceConverter.cs index e80dac7..9c33140 100644 --- a/src/IIIF/IIIF/Serialisation/Deserialisation/ServiceConverter.cs +++ b/src/IIIF/IIIF/Serialisation/Deserialisation/ServiceConverter.cs @@ -35,6 +35,7 @@ public class ServiceConverter : ReadOnlyConverter "AuthTokenService1" => new Auth.V1.AuthTokenService(), "AutoCompleteService1" => new Search.V1.AutoCompleteService(), nameof(ImageService2) => new ImageService2(), + "iiif:Image" => new ImageService2(), _ => null, }; if (service != null) return service; @@ -52,26 +53,30 @@ public class ServiceConverter : ReadOnlyConverter if (service != null) return service; - var profile = jsonObject["profile"].Value(); - service = profile switch + var profileToken = jsonObject["profile"]; + if (profileToken != null) { - Auth.V1.AuthLogoutService.AuthLogout1Profile => new Auth.V1.AuthLogoutService(), - Auth.V1.AuthTokenService.AuthToken1Profile => new Auth.V1.AuthTokenService(), - Auth.V0.AuthLogoutService.AuthLogout0Profile => new Auth.V0.AuthLogoutService(), - Auth.V0.AuthTokenService.AuthToken0Profile => new Auth.V0.AuthTokenService(), - Search.V2.AutoCompleteService.AutoComplete2Profile => new Search.V2.AutoCompleteService(), - Search.V1.AutoCompleteService.AutoCompleteService1Profile => new Search.V1.AutoCompleteService(), - Search.V2.SearchService.Search2Profile => new Search.V2.SearchService(), - _ => null - }; - if (service != null) return service; + var profile = profileToken.Value(); + service = profile switch + { + Auth.V1.AuthLogoutService.AuthLogout1Profile => new Auth.V1.AuthLogoutService(), + Auth.V1.AuthTokenService.AuthToken1Profile => new Auth.V1.AuthTokenService(), + Auth.V0.AuthLogoutService.AuthLogout0Profile => new Auth.V0.AuthLogoutService(), + Auth.V0.AuthTokenService.AuthToken0Profile => new Auth.V0.AuthTokenService(), + Search.V2.AutoCompleteService.AutoComplete2Profile => new Search.V2.AutoCompleteService(), + Search.V1.AutoCompleteService.AutoCompleteService1Profile => new Search.V1.AutoCompleteService(), + Search.V2.SearchService.Search2Profile => new Search.V2.SearchService(), + _ => null + }; + if (service != null) return service; - const string auth0 = "http://iiif.io/api/auth/0/"; - const string auth1 = "http://iiif.io/api/auth/1/"; + 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); + if (profile.StartsWith(auth0)) return new Auth.V0.AuthCookieService(profile); + if (profile.StartsWith(auth1)) return new Auth.V1.AuthCookieService(profile); + } // TODO handle ResourceBase items