Skip to content

Commit

Permalink
Fix issue 383 - Wrong sync API deserialization
Browse files Browse the repository at this point in the history
* adjust models to match sync API response for deserialization

* add runtime type resolution support, adjust method descriptions
  • Loading branch information
pokornyd authored Oct 30, 2023
1 parent 9ab903e commit 6086535
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ public interface IContentItem
/// <summary>
/// Represents system attributes of a content item.
/// </summary>
public IContentItemSystemAttributes System { get; set; }
public IContentItemSystemAttributes System { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Kontent.Ai.Delivery.Abstractions;

/// <summary>
/// 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.
/// </summary>
public interface IDeliverySyncInitResponse : IResponse
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
namespace Kontent.Ai.Delivery.Abstractions;

/// <summary>
/// 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.
/// </summary>
public interface IDeliverySyncResponse : IResponse
{
/// <summary>
/// Gets list of delta update items.
/// Gets the list of items delta updates.
/// </summary>
IList<ISyncItem> SyncItems { get; }
}
25 changes: 5 additions & 20 deletions Kontent.Ai.Delivery.Abstractions/Sync/ISyncItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,14 @@ namespace Kontent.Ai.Delivery.Abstractions;
public interface ISyncItem
{
/// <summary>
/// Gets the content item's codename.
/// Retrieves runtime strongly typed item if CustomTypeProvider is registered, otherwise null.
/// </summary>
string Codename { get; }

/// <summary>
/// Gets the content item's internal ID.
/// </summary>
Guid Id { get; }

/// <summary>
/// Gets the content item's type codename.
/// </summary>
string Type { get; }

/// <summary>
/// Gets the codename of the language that the content is in.
/// </summary>
string Language { get; }

object StronglyTypedData { get; }

/// <summary>
/// Gets the content item's collection codename. For projects without collections enabled, the value is default.
/// Retrieves content item information and element values.
/// </summary>
string Collection { get; }
ISyncItemData Data { get; }

/// <summary>
/// Gets the information whether the content item was modified or deleted since the last synchronization.
Expand Down
14 changes: 14 additions & 0 deletions Kontent.Ai.Delivery.Abstractions/Sync/ISyncItemData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Collections.Generic;

namespace Kontent.Ai.Delivery.Abstractions;

/// <summary>
/// Represents a delta update.
/// </summary>
public interface ISyncItemData : IContentItem
{
/// <summary>
/// Retrieves key:value pairs representing content item elements.
/// </summary>
Dictionary<string, object> Elements { get; }
}
2 changes: 1 addition & 1 deletion Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public async Task<IDeliveryLanguageListingResponse> GetLanguagesAsync(IEnumerabl
/// <summary>
/// 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.
/// </summary>
/// <param name="parameters">A collection of query parameters, for example, for filtering.</param>
/// <param name="parameters">A collection of query parameters, for example to limit synchronization to only a subset of collections or content types.</param>
/// <returns>The <see cref="IDeliverySyncInitResponse"/> instance that represents the sync init response that contains continuation token needed for further sync execution.</returns>
public Task<IDeliverySyncInitResponse> PostSyncInitAsync(IEnumerable<IQueryParameter> parameters = null)
{
Expand Down
84 changes: 63 additions & 21 deletions Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
Expand All @@ -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;
Expand Down Expand Up @@ -1851,41 +1849,85 @@ public async Task SyncApi_PostSyncInitAsync_WithParameters_GetContinuationToken(
}

[Fact]
public async Task SyncApi_GetSyncAsync_GetSyncItems()
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"));

var expectedValue = JObject.Parse(mockedResponse).SelectToken("items").ToObject<IList<SyncItem>>();
var expectedItems = JObject.Parse(mockedResponse).SelectToken("items").ToObject<List<JObject>>();

_mockHttp
.When($"{_baseUrl}/sync")
.WithHeaders("X-Continuation", "token")
.Respond(new[] { new KeyValuePair<string, string>("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);

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 article = sync.SyncItems[i].StronglyTypedData as Article;
var expectedItem = expectedItems[i];
var expectedElementValues = expectedItem["data"]["elements"];

AssertSystemPropertiesEquality(expectedItem["data"]["system"].ToObject<JObject>(), 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_ReturnsGenericData()
{
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<List<JObject>>();

_mockHttp
.When($"{_baseUrl}/sync")
.WithHeaders("X-Continuation", "token")
.Respond(new[] { new KeyValuePair<string, string>("X-Continuation", "token"), }, "application/json", mockedResponse);

var client = InitializeDeliveryClientWithCustomModelProvider(_mockHttp, new PropertyMapper(), new TypeProvider());

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 syncItemData = sync.SyncItems[i].Data;
var expectedItem = expectedItems[i];
var expectedElementValues = expectedItem["data"]["elements"];

AssertSystemPropertiesEquality(expectedItem["data"]["system"].ToObject<JObject>(), 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)
{
Expand Down
48 changes: 37 additions & 11 deletions Kontent.Ai.Delivery.Tests/Fixtures/DeliveryClient/sync.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
}
24 changes: 22 additions & 2 deletions Kontent.Ai.Delivery/DeliveryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,11 @@ public async Task<IDeliveryLanguageListingResponse> GetLanguagesAsync(IEnumerabl
return new DeliveryLanguageListingResponse(response, languages.ToList<ILanguage>(), pagination);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="parameters">A collection of query parameters, for example to limit synchronization to only a subset of collections or content types.</param>
/// <returns>The <see cref="IDeliverySyncInitResponse"/> instance that represents the sync init response that contains continuation token needed for further sync execution.</returns>
public async Task<IDeliverySyncInitResponse> PostSyncInitAsync(IEnumerable<IQueryParameter> parameters = null)
{
var endpointUrl = UrlBuilder.GetSyncInitUrl(parameters);
Expand All @@ -326,9 +331,14 @@ public async Task<IDeliverySyncInitResponse> PostSyncInitAsync(IEnumerable<IQuer

var content = await response.GetJsonContentAsync();
var items = content["items"].ToObject<List<SyncItem>>(Serializer);

return new DeliverySyncInitResponse(response, items.ToList<ISyncItem>());
}

/// <summary>
/// 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.
/// </summary>
/// <returns>The <see cref="IDeliverySyncResponse"/> instance that represents the sync response that contains collection of delta updates and continuation token needed for further sync execution.</returns>
public async Task<IDeliverySyncResponse> GetSyncAsync(string continuationToken)
{
var endpointUrl = UrlBuilder.GetSyncUrl();
Expand All @@ -340,10 +350,20 @@ public async Task<IDeliverySyncResponse> GetSyncAsync(string continuationToken)
}

var content = await response.GetJsonContentAsync();
var items = content["items"].ToObject<List<SyncItem>>(Serializer);
return new DeliverySyncResponse(response, items.ToList<ISyncItem>());
var syncItems = content["items"].ToObject<List<SyncItem>>(Serializer);

var itemModels = await Task.WhenAll(syncItems.Select(async syncItem =>
{
// use TypeProvider from DI container to select a model
var mappedModel = await ModelProvider.GetContentItemModelAsync<object>(JToken.FromObject(syncItem.Data), new JObject());
return new SyncItem(mappedModel, syncItem.Data, syncItem.ChangeType, syncItem.Timestamp);
}));

return new DeliverySyncResponse(response, itemModels);
}


private async Task<ApiResponse> GetDeliveryResponseAsync(string endpointUrl, HttpMethod httpMethod, string continuationToken = null)
{
if (DeliveryOptions.CurrentValue.UsePreviewApi && DeliveryOptions.CurrentValue.UseSecureAccess)
Expand Down
2 changes: 1 addition & 1 deletion Kontent.Ai.Delivery/Sync/DeliverySyncResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal DeliverySyncResponse(IApiResponse response, IList<ISyncItem> syncItems)
SyncItems = syncItems;
}

internal DeliverySyncResponse(IApiResponse response)
internal DeliverySyncResponse(IApiResponse response)
: base(response)
{
}
Expand Down
29 changes: 10 additions & 19 deletions Kontent.Ai.Delivery/Sync/SyncItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,14 @@
namespace Kontent.Ai.Delivery.Sync;

/// <inheritdoc/>
public class SyncItem : ISyncItem
internal sealed class SyncItem : ISyncItem
{
/// <inheritdoc/>
[JsonProperty("codename")]
public string Codename { get; internal set; }
public object StronglyTypedData { get; internal set; }

/// <inheritdoc/>
[JsonProperty("id")]
public Guid Id { get; internal set; }

/// <inheritdoc/>
[JsonProperty("type")]
public string Type { get; internal set; }

/// <inheritdoc/>
[JsonProperty("language")]
public string Language { get; internal set; }

/// <inheritdoc/>
[JsonProperty("collection")]
public string Collection { get; internal set; }
[JsonProperty("data")]
public ISyncItemData Data { get; internal set; }

/// <inheritdoc/>
[JsonProperty("change_type")]
Expand All @@ -36,10 +23,14 @@ public class SyncItem : ISyncItem
public DateTime Timestamp { get; internal set; }

/// <summary>
/// Constructor used for deserialization (e.g. for caching purposes), contains no logic.
/// Initializes a new instance of <see cref="SyncItem"/> class.
/// </summary>
[JsonConstructor]
public SyncItem()
public SyncItem(object stronglyTypedData, ISyncItemData data, string changeType, DateTime timestamp)
{
StronglyTypedData = stronglyTypedData;
Data = data;
ChangeType = changeType;
Timestamp = timestamp;
}
}
Loading

0 comments on commit 6086535

Please sign in to comment.