From 0fd8e3b3ed7efa9d91b197ae63a2bfe2457baa28 Mon Sep 17 00:00:00 2001 From: Daniel Pokorny Date: Tue, 24 Oct 2023 03:17:10 +0200 Subject: [PATCH 1/4] adjust models to match sync API response for serialization --- .../Sync/ISyncItem.cs | 25 +------------ .../Sync/ISyncItemData.cs | 12 +++++++ Kontent.Ai.Delivery/Sync/SyncItem.cs | 36 +++++-------------- Kontent.Ai.Delivery/Sync/SyncItemData.cs | 22 ++++++++++++ 4 files changed, 44 insertions(+), 51 deletions(-) create mode 100644 Kontent.Ai.Delivery.Abstractions/Sync/ISyncItemData.cs create mode 100644 Kontent.Ai.Delivery/Sync/SyncItemData.cs diff --git a/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItem.cs b/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItem.cs index 091c52b4..ea08ef70 100644 --- a/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItem.cs +++ b/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItem.cs @@ -7,30 +7,7 @@ namespace Kontent.Ai.Delivery.Abstractions; /// public interface ISyncItem { - /// - /// Gets the content item's codename. - /// - string Codename { get; } - - /// - /// Gets the content item's internal ID. - /// - Guid Id { get; } - - /// - /// Gets the content item's type codename. - /// - string Type { get; } - - /// - /// Gets the codename of the language that the content is in. - /// - string Language { get; } - - /// - /// Gets the content item's collection codename. For projects without collections enabled, the value is default. - /// - string Collection { get; } + ISyncItemData Data { get; } /// /// Gets the information whether the content item was modified or deleted since the last synchronization. diff --git a/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItemData.cs b/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItemData.cs new file mode 100644 index 00000000..971c1b63 --- /dev/null +++ b/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItemData.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Kontent.Ai.Delivery.Abstractions; + +/// +/// Represents a delta update. +/// +public interface ISyncItemData +{ + IContentItemSystemAttributes System { get; } + object Elements { get; } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery/Sync/SyncItem.cs b/Kontent.Ai.Delivery/Sync/SyncItem.cs index 319010bb..945ea4c3 100644 --- a/Kontent.Ai.Delivery/Sync/SyncItem.cs +++ b/Kontent.Ai.Delivery/Sync/SyncItem.cs @@ -1,45 +1,27 @@ using System; +using System.Collections.Generic; using Kontent.Ai.Delivery.Abstractions; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Kontent.Ai.Delivery.Sync; /// -public class SyncItem : ISyncItem +internal sealed class SyncItem : ISyncItem { - /// - [JsonProperty("codename")] - public string Codename { get; internal set; } + [JsonProperty("data")] + public ISyncItemData Data { get; internal set; } - /// - [JsonProperty("id")] - public Guid Id { get; internal set; } - - /// - [JsonProperty("type")] - public string Type { get; internal set; } - - /// - [JsonProperty("language")] - public string Language { get; internal set; } - - /// - [JsonProperty("collection")] - public string Collection { get; internal set; } - - /// [JsonProperty("change_type")] public string ChangeType { get; internal set; } - /// [JsonProperty("timestamp")] public DateTime Timestamp { get; internal set; } - /// - /// Constructor used for deserialization (e.g. for caching purposes), contains no logic. - /// [JsonConstructor] - public SyncItem() - { + public SyncItem(ISyncItemData data, string changeType, DateTime timestamp) { + Data = data; + ChangeType = changeType; + Timestamp = timestamp; } } \ No newline at end of file diff --git a/Kontent.Ai.Delivery/Sync/SyncItemData.cs b/Kontent.Ai.Delivery/Sync/SyncItemData.cs new file mode 100644 index 00000000..ea0966a7 --- /dev/null +++ b/Kontent.Ai.Delivery/Sync/SyncItemData.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; +using Kontent.Ai.Delivery.Abstractions; +using Kontent.Ai.Delivery.ContentTypes.Element; +using Newtonsoft.Json; + +namespace Kontent.Ai.Delivery.Sync +{ + public class SyncItemData : ISyncItemData + { + [JsonProperty("system")] + public IContentItemSystemAttributes System { get; internal set; } + + [JsonProperty("elements")] + public object Elements { get; internal set; } + + [JsonConstructor] + public SyncItemData() + { + } + } +} \ No newline at end of file From 45b16e59d3c08887c85ba84d26f87cad40d18d9b Mon Sep 17 00:00:00 2001 From: Daniel Pokorny Date: Tue, 24 Oct 2023 06:36:37 +0200 Subject: [PATCH 2/4] add type resolution support, refactor, adjust method descriptions --- .../Sync/IDeliverySyncInitResponse.cs | 2 +- .../Sync/ISyncItem.cs | 5 +++- .../Sync/ISyncItemData.cs | 12 --------- .../DeliveryClientCache.cs | 2 +- Kontent.Ai.Delivery/DeliveryClient.cs | 26 +++++++++++++++++-- .../Sync/DeliverySyncResponse.cs | 2 +- Kontent.Ai.Delivery/Sync/SyncItem.cs | 23 ++++++++-------- Kontent.Ai.Delivery/Sync/SyncItemData.cs | 22 ---------------- 8 files changed, 43 insertions(+), 51 deletions(-) delete mode 100644 Kontent.Ai.Delivery.Abstractions/Sync/ISyncItemData.cs delete mode 100644 Kontent.Ai.Delivery/Sync/SyncItemData.cs diff --git a/Kontent.Ai.Delivery.Abstractions/Sync/IDeliverySyncInitResponse.cs b/Kontent.Ai.Delivery.Abstractions/Sync/IDeliverySyncInitResponse.cs index 709aac58..57979f9b 100644 --- a/Kontent.Ai.Delivery.Abstractions/Sync/IDeliverySyncInitResponse.cs +++ b/Kontent.Ai.Delivery.Abstractions/Sync/IDeliverySyncInitResponse.cs @@ -3,7 +3,7 @@ namespace Kontent.Ai.Delivery.Abstractions; /// -/// Represents a response from Kontent.ai Delivery API that contains a continuation token and . +/// Represents a response from Kontent.ai Sync API. Response includes continuation token for subsequent synchronization calls. Sync initialization should always return an empty list. /// public interface IDeliverySyncInitResponse : IResponse { diff --git a/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItem.cs b/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItem.cs index ea08ef70..5cbb316e 100644 --- a/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItem.cs +++ b/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItem.cs @@ -7,7 +7,10 @@ namespace Kontent.Ai.Delivery.Abstractions; /// public interface ISyncItem { - ISyncItemData Data { get; } + /// + /// Retrieves content item information and element values. + /// + object Data { get; } /// /// Gets the information whether the content item was modified or deleted since the last synchronization. diff --git a/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItemData.cs b/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItemData.cs deleted file mode 100644 index 971c1b63..00000000 --- a/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItemData.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; - -namespace Kontent.Ai.Delivery.Abstractions; - -/// -/// Represents a delta update. -/// -public interface ISyncItemData -{ - IContentItemSystemAttributes System { get; } - object Elements { get; } -} \ No newline at end of file diff --git a/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs b/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs index 7be940a2..64b78fb6 100644 --- a/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs +++ b/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs @@ -160,7 +160,7 @@ public async Task GetLanguagesAsync(IEnumerabl /// /// Initializes synchronization of changes in content items based on the specified parameters. After the initialization, you'll get an X-Continuation token in the response. /// - /// A collection of query parameters, for example, for filtering. + /// A collection of query parameters, for example to limit synchronization to only a subset of collections or content types. /// The instance that represents the sync init response that contains continuation token needed for further sync execution. public Task PostSyncInitAsync(IEnumerable parameters = null) { diff --git a/Kontent.Ai.Delivery/DeliveryClient.cs b/Kontent.Ai.Delivery/DeliveryClient.cs index f5b62464..b4903fa0 100644 --- a/Kontent.Ai.Delivery/DeliveryClient.cs +++ b/Kontent.Ai.Delivery/DeliveryClient.cs @@ -314,6 +314,11 @@ public async Task GetLanguagesAsync(IEnumerabl return new DeliveryLanguageListingResponse(response, languages.ToList(), pagination); } + /// + /// Initializes synchronization of changes in content items based on the specified parameters. After the initialization, you'll get an X-Continuation token in the response. + /// + /// A collection of query parameters, for example to limit synchronization to only a subset of collections or content types. + /// The instance that represents the sync init response that contains continuation token needed for further sync execution. public async Task PostSyncInitAsync(IEnumerable parameters = null) { var endpointUrl = UrlBuilder.GetSyncInitUrl(parameters); @@ -329,6 +334,10 @@ public async Task PostSyncInitAsync(IEnumerable()); } + /// + /// Retrieve a list of delta updates to recently changed content items in the specified project. The types of items you get is determined by the X-Continuation token you use. + /// + /// The instance that represents the sync response that contains collection of delta updates and continuation token needed for further sync execution. public async Task GetSyncAsync(string continuationToken) { var endpointUrl = UrlBuilder.GetSyncUrl(); @@ -340,10 +349,23 @@ public async Task GetSyncAsync(string continuationToken) } var content = await response.GetJsonContentAsync(); - var items = content["items"].ToObject>(Serializer); - return new DeliverySyncResponse(response, items.ToList()); + var syncItems = content["items"].ToObject>(Serializer); + + var itemModels = await Task.WhenAll(syncItems.Select(async syncItem => + { + // use TypeProvider from DI container to select a model + var mappedModel = await ModelProvider.GetContentItemModelAsync(syncItem.Data, new JObject()); + if (mappedModel == null) + { + // return JObject if no suitable model is found + return new SyncItem(syncItem.Data, syncItem.ChangeType, syncItem.Timestamp); + } + return new SyncItem(mappedModel, syncItem.ChangeType, syncItem.Timestamp); + })); + return new DeliverySyncResponse(response, itemModels); } + private async Task GetDeliveryResponseAsync(string endpointUrl, HttpMethod httpMethod, string continuationToken = null) { if (DeliveryOptions.CurrentValue.UsePreviewApi && DeliveryOptions.CurrentValue.UseSecureAccess) diff --git a/Kontent.Ai.Delivery/Sync/DeliverySyncResponse.cs b/Kontent.Ai.Delivery/Sync/DeliverySyncResponse.cs index 203dde69..e86bb0ac 100644 --- a/Kontent.Ai.Delivery/Sync/DeliverySyncResponse.cs +++ b/Kontent.Ai.Delivery/Sync/DeliverySyncResponse.cs @@ -22,7 +22,7 @@ internal DeliverySyncResponse(IApiResponse response, IList syncItems) SyncItems = syncItems; } - internal DeliverySyncResponse(IApiResponse response) + internal DeliverySyncResponse(IApiResponse response) : base(response) { } diff --git a/Kontent.Ai.Delivery/Sync/SyncItem.cs b/Kontent.Ai.Delivery/Sync/SyncItem.cs index 945ea4c3..6491ea09 100644 --- a/Kontent.Ai.Delivery/Sync/SyncItem.cs +++ b/Kontent.Ai.Delivery/Sync/SyncItem.cs @@ -7,21 +7,22 @@ namespace Kontent.Ai.Delivery.Sync; /// -internal sealed class SyncItem : ISyncItem +/// +/// Constructor used for deserialization (e.g. for caching purposes), contains no logic. +/// +[method: JsonConstructor] +/// <> +internal sealed class SyncItem(object data, string changeType, DateTime timestamp) : ISyncItem { + /// [JsonProperty("data")] - public ISyncItemData Data { get; internal set; } + public object Data { get; internal set; } = data; + /// [JsonProperty("change_type")] - public string ChangeType { get; internal set; } + public string ChangeType { get; internal set; } = changeType; + /// [JsonProperty("timestamp")] - public DateTime Timestamp { get; internal set; } - - [JsonConstructor] - public SyncItem(ISyncItemData data, string changeType, DateTime timestamp) { - Data = data; - ChangeType = changeType; - Timestamp = timestamp; - } + public DateTime Timestamp { get; internal set; } = timestamp; } \ No newline at end of file diff --git a/Kontent.Ai.Delivery/Sync/SyncItemData.cs b/Kontent.Ai.Delivery/Sync/SyncItemData.cs deleted file mode 100644 index ea0966a7..00000000 --- a/Kontent.Ai.Delivery/Sync/SyncItemData.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Kontent.Ai.Delivery.Abstractions; -using Kontent.Ai.Delivery.ContentTypes.Element; -using Newtonsoft.Json; - -namespace Kontent.Ai.Delivery.Sync -{ - public class SyncItemData : ISyncItemData - { - [JsonProperty("system")] - public IContentItemSystemAttributes System { get; internal set; } - - [JsonProperty("elements")] - public object Elements { get; internal set; } - - [JsonConstructor] - public SyncItemData() - { - } - } -} \ No newline at end of file From 46bb20c21e7c0967471981738877ddf81941035d Mon Sep 17 00:00:00 2001 From: Daniel Pokorny Date: Tue, 24 Oct 2023 08:34:01 +0200 Subject: [PATCH 3/4] update tests --- .../Sync/IDeliverySyncResponse.cs | 4 +- .../DeliveryClientTests.cs | 90 +++++++++++++++---- .../Fixtures/DeliveryClient/sync.json | 48 +++++++--- Kontent.Ai.Delivery/Sync/SyncItem.cs | 23 +++-- 4 files changed, 124 insertions(+), 41 deletions(-) diff --git a/Kontent.Ai.Delivery.Abstractions/Sync/IDeliverySyncResponse.cs b/Kontent.Ai.Delivery.Abstractions/Sync/IDeliverySyncResponse.cs index e5901236..c2735a34 100644 --- a/Kontent.Ai.Delivery.Abstractions/Sync/IDeliverySyncResponse.cs +++ b/Kontent.Ai.Delivery.Abstractions/Sync/IDeliverySyncResponse.cs @@ -3,12 +3,12 @@ namespace Kontent.Ai.Delivery.Abstractions; /// -/// Represents a response from Kontent.ai Delivery API that contains a taxonomy group. +/// Represents a response from Kontent.ai Sync API that contains recently updated items. Response includes continuation token for subsequent synchronization calls. /// public interface IDeliverySyncResponse : IResponse { /// - /// Gets list of delta update items. + /// Gets the list of items delta updates. /// IList SyncItems { get; } } \ No newline at end of file diff --git a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs index 96596bf1..626bee26 100644 --- a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs +++ b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs @@ -1851,18 +1851,62 @@ public async Task SyncApi_PostSyncInitAsync_WithParameters_GetContinuationToken( } [Fact] - public async Task SyncApi_GetSyncAsync_GetSyncItems() + public async Task SyncApi_GetSyncAsync_GetSyncItems_WithTypeProvider() { var mockedResponse = await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}sync.json")); - var expectedValue = JObject.Parse(mockedResponse).SelectToken("items").ToObject>(); - + var expectedItems = JObject.Parse(mockedResponse).SelectToken("items").ToObject>(); + _mockHttp .When($"{_baseUrl}/sync") .WithHeaders("X-Continuation", "token") .Respond(new[] { new KeyValuePair("X-Continuation", "token"), }, "application/json", mockedResponse); - var client = InitializeDeliveryClientWithCustomModelProvider(_mockHttp); + var client = InitializeDeliveryClientWithACustomTypeProvider(_mockHttp); + + var sync = await client.GetSyncAsync("token"); + + Assert.NotNull(sync.ApiResponse.ContinuationToken); + + Assert.Equal(2, sync.SyncItems.Count); + + for (int i = 0; i < expectedItems.Count; i++) + { + var expectedItem = expectedItems[i]; + var syncItem = (Article)sync.SyncItems[i].Data; + + var expectedSystemValues = expectedItem["data"]["system"]; + var expectedElementValues = expectedItem["data"]["elements"]; + var syncItemSystemValues = syncItem.System; + + Assert.Equal(expectedSystemValues["codename"].ToString(), syncItemSystemValues.Codename.ToString()); + Assert.Equal(expectedSystemValues["name"].ToString(), syncItemSystemValues.Name.ToString()); + Assert.Equal(expectedSystemValues["id"].ToString(), syncItemSystemValues.Id.ToString()); + Assert.Equal(expectedSystemValues["type"].ToString(), syncItemSystemValues.Type.ToString()); + Assert.Equal(expectedSystemValues["language"].ToString(), syncItemSystemValues.Language.ToString()); + Assert.Equal(expectedSystemValues["collection"].ToString(), syncItemSystemValues.Collection.ToString()); + Assert.Equal(expectedSystemValues["workflow_step"].ToString(), syncItemSystemValues.WorkflowStep.ToString()); + + Assert.Equal(expectedElementValues["title"]["value"].ToString(), syncItem.Title); + + Assert.Equal(expectedItem["change_type"].ToString(), sync.SyncItems[i].ChangeType); + Assert.Equal(DateTime.Parse(expectedItem["timestamp"].ToString()), DateTime.Parse(sync.SyncItems[i].Timestamp.ToString())); + } + } + + [Fact] + public async Task SyncApi_GetSyncAsync_GetSyncItems_WithoutTypeProvider() + { + var mockedResponse = await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}sync.json")); + + var expectedItems = JObject.Parse(mockedResponse).SelectToken("items").ToObject>(); + + _mockHttp + .When($"{_baseUrl}/sync") + .WithHeaders("X-Continuation", "token") + .Respond(new[] { new KeyValuePair("X-Continuation", "token"), }, "application/json", mockedResponse); + + var client = InitializeDeliveryClientWithCustomModelProvider(_mockHttp, new PropertyMapper(), new TypeProvider()); var sync = await client.GetSyncAsync("token"); @@ -1870,23 +1914,31 @@ public async Task SyncApi_GetSyncAsync_GetSyncItems() Assert.Equal(2, sync.SyncItems.Count); - Assert.Equal(expectedValue[0].Codename, sync.SyncItems[0].Codename); - Assert.Equal(expectedValue[0].Id, sync.SyncItems[0].Id); - Assert.Equal(expectedValue[0].Type, sync.SyncItems[0].Type); - Assert.Equal(expectedValue[0].Language, sync.SyncItems[0].Language); - Assert.Equal(expectedValue[0].Collection, sync.SyncItems[0].Collection); - Assert.Equal(expectedValue[0].ChangeType, sync.SyncItems[0].ChangeType); - Assert.Equal(expectedValue[0].Timestamp, sync.SyncItems[0].Timestamp); - - Assert.Equal(expectedValue[1].Codename, sync.SyncItems[1].Codename); - Assert.Equal(expectedValue[1].Id, sync.SyncItems[1].Id); - Assert.Equal(expectedValue[1].Type, sync.SyncItems[1].Type); - Assert.Equal(expectedValue[1].Language, sync.SyncItems[1].Language); - Assert.Equal(expectedValue[1].Collection, sync.SyncItems[1].Collection); - Assert.Equal(expectedValue[1].ChangeType, sync.SyncItems[1].ChangeType); - Assert.Equal(expectedValue[1].Timestamp, sync.SyncItems[1].Timestamp); + for (int i = 0; i < expectedItems.Count; i++) + { + var expectedItem = expectedItems[i]; + var syncItem = (JObject)sync.SyncItems[i].Data; + + var expectedSystemValues = expectedItem["data"]["system"]; + var expectedElementValues = expectedItem["data"]["elements"]; + var syncItemSystemValues = syncItem["system"]; + + Assert.Equal(expectedSystemValues["codename"].ToString(), syncItemSystemValues["codename"].ToString()); + Assert.Equal(expectedSystemValues["name"].ToString(), syncItemSystemValues["name"].ToString()); + Assert.Equal(expectedSystemValues["id"].ToString(), syncItemSystemValues["id"].ToString()); + Assert.Equal(expectedSystemValues["type"].ToString(), syncItemSystemValues["type"].ToString()); + Assert.Equal(expectedSystemValues["language"].ToString(), syncItemSystemValues["language"].ToString()); + Assert.Equal(expectedSystemValues["collection"].ToString(), syncItemSystemValues["collection"].ToString()); + Assert.Equal(expectedSystemValues["workflow_step"].ToString(), syncItemSystemValues["workflow_step"].ToString()); + + Assert.Equal(expectedElementValues["title"]["value"].ToString(), syncItem["elements"]["title"]["value"]); + + Assert.Equal(expectedItem["change_type"].ToString(), sync.SyncItems[i].ChangeType); + Assert.Equal(DateTime.Parse(expectedItem["timestamp"].ToString()), DateTime.Parse(sync.SyncItems[i].Timestamp.ToString())); + } } + private DeliveryClient InitializeDeliveryClientWithACustomTypeProvider(MockHttpMessageHandler handler) { var customTypeProvider = new CustomTypeProvider(); diff --git a/Kontent.Ai.Delivery.Tests/Fixtures/DeliveryClient/sync.json b/Kontent.Ai.Delivery.Tests/Fixtures/DeliveryClient/sync.json index 1b377b27..15332266 100644 --- a/Kontent.Ai.Delivery.Tests/Fixtures/DeliveryClient/sync.json +++ b/Kontent.Ai.Delivery.Tests/Fixtures/DeliveryClient/sync.json @@ -1,22 +1,48 @@ { "items": [ { - "codename": "hello_world", - "id": "7adfb82a-1386-4228-bcc2-45073a0355f6", - "type": "article", - "language": "default", - "collection": "default", + "data": { + "system": { + "codename": "hello_world", + "name": "Hello world!", + "id": "7adfb82a-1386-4228-bcc2-45073a0355f6", + "type": "article", + "language": "default", + "collection": "default", + "workflow_step": "published" + }, + "elements": { + "title": { + "type": "text", + "name": "Title", + "value": "Hello world!" + } + } + }, "change_type": "changed", "timestamp": "2022-10-06T08:38:40.0088127Z" }, { - "codename": "bye__world", - "id": "42a3cfbd-4967-43e7-987b-e1e69c483e26", - "type": "article", - "language": "default", - "collection": "default", + "data": { + "system": { + "codename": "bye_world", + "name": "Bye world!", + "id": "42a3cfbd-4967-43e7-987b-e1e69c483e26", + "type": "article", + "language": "default", + "collection": "default", + "workflow_step": "published" + }, + "elements": { + "title": { + "type": "text", + "name": "Title", + "value": "Bye world!" + } + } + }, "change_type": "deleted", "timestamp": "2022-10-06T08:38:47.3613558Z" } ] -} \ No newline at end of file +} diff --git a/Kontent.Ai.Delivery/Sync/SyncItem.cs b/Kontent.Ai.Delivery/Sync/SyncItem.cs index 6491ea09..589ec28d 100644 --- a/Kontent.Ai.Delivery/Sync/SyncItem.cs +++ b/Kontent.Ai.Delivery/Sync/SyncItem.cs @@ -7,22 +7,27 @@ namespace Kontent.Ai.Delivery.Sync; /// -/// -/// Constructor used for deserialization (e.g. for caching purposes), contains no logic. -/// -[method: JsonConstructor] -/// <> -internal sealed class SyncItem(object data, string changeType, DateTime timestamp) : ISyncItem +internal sealed class SyncItem : ISyncItem { /// [JsonProperty("data")] - public object Data { get; internal set; } = data; + public object Data { get; internal set; } /// [JsonProperty("change_type")] - public string ChangeType { get; internal set; } = changeType; + public string ChangeType { get; internal set; } /// [JsonProperty("timestamp")] - public DateTime Timestamp { get; internal set; } = timestamp; + public DateTime Timestamp { get; internal set; } + + /// + /// Constructor used for deserialization (e.g. for caching purposes), contains no logic. + /// + [JsonConstructor] + public SyncItem(object data, string changeType, DateTime timestamp) { + Data = data; + ChangeType = changeType; + Timestamp = timestamp; + } } \ No newline at end of file From c726a1b384b5f3c9ca0fddfc62fe2b2c7c7e854c Mon Sep 17 00:00:00 2001 From: Daniel Pokorny Date: Sun, 29 Oct 2023 18:01:11 +0100 Subject: [PATCH 4/4] refactor SyncItem model, adjust tests --- .../ContentItems/IContentItem.cs | 2 +- .../Sync/ISyncItem.cs | 7 ++- .../Sync/ISyncItemData.cs | 14 +++++ .../DeliveryClientTests.cs | 58 ++++++++----------- Kontent.Ai.Delivery/DeliveryClient.cs | 12 ++-- Kontent.Ai.Delivery/Sync/SyncItem.cs | 13 +++-- Kontent.Ai.Delivery/Sync/SyncItemData.cs | 25 ++++++++ 7 files changed, 83 insertions(+), 48 deletions(-) create mode 100644 Kontent.Ai.Delivery.Abstractions/Sync/ISyncItemData.cs create mode 100644 Kontent.Ai.Delivery/Sync/SyncItemData.cs diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/IContentItem.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/IContentItem.cs index e9523383..15949eab 100644 --- a/Kontent.Ai.Delivery.Abstractions/ContentItems/IContentItem.cs +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/IContentItem.cs @@ -8,5 +8,5 @@ public interface IContentItem /// /// Represents system attributes of a content item. /// - public IContentItemSystemAttributes System { get; set; } + public IContentItemSystemAttributes System { get; } } diff --git a/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItem.cs b/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItem.cs index 5cbb316e..c48dd80c 100644 --- a/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItem.cs +++ b/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItem.cs @@ -7,10 +7,15 @@ namespace Kontent.Ai.Delivery.Abstractions; /// public interface ISyncItem { + /// + /// Retrieves runtime strongly typed item if CustomTypeProvider is registered, otherwise null. + /// + object StronglyTypedData { get; } + /// /// Retrieves content item information and element values. /// - object Data { get; } + ISyncItemData Data { get; } /// /// Gets the information whether the content item was modified or deleted since the last synchronization. diff --git a/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItemData.cs b/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItemData.cs new file mode 100644 index 00000000..6f3990f8 --- /dev/null +++ b/Kontent.Ai.Delivery.Abstractions/Sync/ISyncItemData.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Kontent.Ai.Delivery.Abstractions; + +/// +/// Represents a delta update. +/// +public interface ISyncItemData : IContentItem +{ + /// + /// Retrieves key:value pairs representing content item elements. + /// + Dictionary Elements { get; } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs index 626bee26..baef9b17 100644 --- a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs +++ b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -14,7 +13,6 @@ using Kontent.Ai.Delivery.ContentItems; using Kontent.Ai.Delivery.ContentItems.RichText.Blocks; using Kontent.Ai.Delivery.SharedModels; -using Kontent.Ai.Delivery.Sync; using Kontent.Ai.Delivery.Tests.Factories; using Kontent.Ai.Delivery.Tests.Models; using Kontent.Ai.Delivery.Tests.Models.ContentTypes; @@ -1851,7 +1849,7 @@ public async Task SyncApi_PostSyncInitAsync_WithParameters_GetContinuationToken( } [Fact] - public async Task SyncApi_GetSyncAsync_GetSyncItems_WithTypeProvider() + public async Task SyncApi_GetSyncAsync_GetSyncItems_WithTypeProvider_ReturnsStronglyTypedData() { var mockedResponse = await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}sync.json")); @@ -1872,30 +1870,20 @@ public async Task SyncApi_GetSyncAsync_GetSyncItems_WithTypeProvider() for (int i = 0; i < expectedItems.Count; i++) { + var article = sync.SyncItems[i].StronglyTypedData as Article; var expectedItem = expectedItems[i]; - var syncItem = (Article)sync.SyncItems[i].Data; - - var expectedSystemValues = expectedItem["data"]["system"]; var expectedElementValues = expectedItem["data"]["elements"]; - var syncItemSystemValues = syncItem.System; - - Assert.Equal(expectedSystemValues["codename"].ToString(), syncItemSystemValues.Codename.ToString()); - Assert.Equal(expectedSystemValues["name"].ToString(), syncItemSystemValues.Name.ToString()); - Assert.Equal(expectedSystemValues["id"].ToString(), syncItemSystemValues.Id.ToString()); - Assert.Equal(expectedSystemValues["type"].ToString(), syncItemSystemValues.Type.ToString()); - Assert.Equal(expectedSystemValues["language"].ToString(), syncItemSystemValues.Language.ToString()); - Assert.Equal(expectedSystemValues["collection"].ToString(), syncItemSystemValues.Collection.ToString()); - Assert.Equal(expectedSystemValues["workflow_step"].ToString(), syncItemSystemValues.WorkflowStep.ToString()); - - Assert.Equal(expectedElementValues["title"]["value"].ToString(), syncItem.Title); - + + AssertSystemPropertiesEquality(expectedItem["data"]["system"].ToObject(), article.System); + Assert.NotNull(article); + Assert.Equal(expectedElementValues["title"]["value"].ToString(), article.Title); Assert.Equal(expectedItem["change_type"].ToString(), sync.SyncItems[i].ChangeType); Assert.Equal(DateTime.Parse(expectedItem["timestamp"].ToString()), DateTime.Parse(sync.SyncItems[i].Timestamp.ToString())); } } [Fact] - public async Task SyncApi_GetSyncAsync_GetSyncItems_WithoutTypeProvider() + public async Task SyncApi_GetSyncAsync_GetSyncItems_WithoutTypeProvider_ReturnsGenericData() { var mockedResponse = await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}sync.json")); @@ -1916,28 +1904,30 @@ public async Task SyncApi_GetSyncAsync_GetSyncItems_WithoutTypeProvider() for (int i = 0; i < expectedItems.Count; i++) { + var syncItemData = sync.SyncItems[i].Data; var expectedItem = expectedItems[i]; - var syncItem = (JObject)sync.SyncItems[i].Data; - - var expectedSystemValues = expectedItem["data"]["system"]; var expectedElementValues = expectedItem["data"]["elements"]; - var syncItemSystemValues = syncItem["system"]; - - Assert.Equal(expectedSystemValues["codename"].ToString(), syncItemSystemValues["codename"].ToString()); - Assert.Equal(expectedSystemValues["name"].ToString(), syncItemSystemValues["name"].ToString()); - Assert.Equal(expectedSystemValues["id"].ToString(), syncItemSystemValues["id"].ToString()); - Assert.Equal(expectedSystemValues["type"].ToString(), syncItemSystemValues["type"].ToString()); - Assert.Equal(expectedSystemValues["language"].ToString(), syncItemSystemValues["language"].ToString()); - Assert.Equal(expectedSystemValues["collection"].ToString(), syncItemSystemValues["collection"].ToString()); - Assert.Equal(expectedSystemValues["workflow_step"].ToString(), syncItemSystemValues["workflow_step"].ToString()); - - Assert.Equal(expectedElementValues["title"]["value"].ToString(), syncItem["elements"]["title"]["value"]); - + + AssertSystemPropertiesEquality(expectedItem["data"]["system"].ToObject(), sync.SyncItems[i].Data.System); + Assert.Null(sync.SyncItems[i].StronglyTypedData); + Assert.NotNull(syncItemData.Elements["title"]); + Assert.Equal(expectedElementValues["title"], syncItemData.Elements["title"]); Assert.Equal(expectedItem["change_type"].ToString(), sync.SyncItems[i].ChangeType); Assert.Equal(DateTime.Parse(expectedItem["timestamp"].ToString()), DateTime.Parse(sync.SyncItems[i].Timestamp.ToString())); } } + private void AssertSystemPropertiesEquality(JObject expectedSystemValues, IContentItemSystemAttributes system) + { + Assert.Equal(expectedSystemValues["codename"].ToString(), system.Codename.ToString()); + Assert.Equal(expectedSystemValues["name"].ToString(), system.Name.ToString()); + Assert.Equal(expectedSystemValues["id"].ToString(), system.Id.ToString()); + Assert.Equal(expectedSystemValues["type"].ToString(), system.Type.ToString()); + Assert.Equal(expectedSystemValues["language"].ToString(), system.Language.ToString()); + Assert.Equal(expectedSystemValues["collection"].ToString(), system.Collection.ToString()); + Assert.Equal(expectedSystemValues["workflow_step"].ToString(), system.WorkflowStep.ToString()); + } + private DeliveryClient InitializeDeliveryClientWithACustomTypeProvider(MockHttpMessageHandler handler) { diff --git a/Kontent.Ai.Delivery/DeliveryClient.cs b/Kontent.Ai.Delivery/DeliveryClient.cs index b4903fa0..9ee0a959 100644 --- a/Kontent.Ai.Delivery/DeliveryClient.cs +++ b/Kontent.Ai.Delivery/DeliveryClient.cs @@ -331,6 +331,7 @@ public async Task PostSyncInitAsync(IEnumerable>(Serializer); + return new DeliverySyncInitResponse(response, items.ToList()); } @@ -354,14 +355,11 @@ public async Task GetSyncAsync(string continuationToken) var itemModels = await Task.WhenAll(syncItems.Select(async syncItem => { // use TypeProvider from DI container to select a model - var mappedModel = await ModelProvider.GetContentItemModelAsync(syncItem.Data, new JObject()); - if (mappedModel == null) - { - // return JObject if no suitable model is found - return new SyncItem(syncItem.Data, syncItem.ChangeType, syncItem.Timestamp); - } - return new SyncItem(mappedModel, syncItem.ChangeType, syncItem.Timestamp); + var mappedModel = await ModelProvider.GetContentItemModelAsync(JToken.FromObject(syncItem.Data), new JObject()); + + return new SyncItem(mappedModel, syncItem.Data, syncItem.ChangeType, syncItem.Timestamp); })); + return new DeliverySyncResponse(response, itemModels); } diff --git a/Kontent.Ai.Delivery/Sync/SyncItem.cs b/Kontent.Ai.Delivery/Sync/SyncItem.cs index 589ec28d..4d4e6f13 100644 --- a/Kontent.Ai.Delivery/Sync/SyncItem.cs +++ b/Kontent.Ai.Delivery/Sync/SyncItem.cs @@ -1,17 +1,18 @@ using System; -using System.Collections.Generic; using Kontent.Ai.Delivery.Abstractions; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Kontent.Ai.Delivery.Sync; /// internal sealed class SyncItem : ISyncItem { + /// + public object StronglyTypedData { get; internal set; } + /// [JsonProperty("data")] - public object Data { get; internal set; } + public ISyncItemData Data { get; internal set; } /// [JsonProperty("change_type")] @@ -22,10 +23,12 @@ internal sealed class SyncItem : ISyncItem public DateTime Timestamp { get; internal set; } /// - /// Constructor used for deserialization (e.g. for caching purposes), contains no logic. + /// Initializes a new instance of class. /// [JsonConstructor] - public SyncItem(object data, string changeType, DateTime timestamp) { + public SyncItem(object stronglyTypedData, ISyncItemData data, string changeType, DateTime timestamp) + { + StronglyTypedData = stronglyTypedData; Data = data; ChangeType = changeType; Timestamp = timestamp; diff --git a/Kontent.Ai.Delivery/Sync/SyncItemData.cs b/Kontent.Ai.Delivery/Sync/SyncItemData.cs new file mode 100644 index 00000000..83193863 --- /dev/null +++ b/Kontent.Ai.Delivery/Sync/SyncItemData.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Kontent.Ai.Delivery.Abstractions; +using Newtonsoft.Json; + +namespace Kontent.Ai.Delivery.Sync +{ + internal sealed class SyncItemData : ISyncItemData + { + /// + [JsonProperty("system")] + public IContentItemSystemAttributes System { get; internal set; } + + /// + [JsonProperty("elements")] + public Dictionary Elements { get; internal set; } + + /// + /// Constructor used for deserialization. Contains no logic. + /// + [JsonConstructor] + public SyncItemData() + { + } + } +} \ No newline at end of file