From 961c626d1180273e6733de3c04737f85c764f1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Mon, 27 Feb 2023 19:12:48 +0100 Subject: [PATCH 01/34] prepare the concept of the dynamic model mapping with simple text --- .../ContentItems/Elements/IElements.cs | 11 +++++ .../DeliveryClientTests.cs | 15 +++++++ .../Models/DynamicContentItemModel.cs | 10 +++++ .../ContentItems/ModelProvider.cs | 40 ++++++++++++++++++- 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IElements.cs create mode 100644 Kontent.Ai.Delivery.Tests/Models/DynamicContentItemModel.cs diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IElements.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IElements.cs new file mode 100644 index 00000000..03d1f9f5 --- /dev/null +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IElements.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Kontent.Ai.Delivery.Abstractions +{ + // TODO validate this to get whole element context + // public class IElements : Dictionary> + public class IElements : Dictionary + { + + } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs index f77e8c87..5a11ce25 100644 --- a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs +++ b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs @@ -936,6 +936,7 @@ public async Task GetStronglyTypedItemsResponse() // Assert Assert.True(items.All(i => i.GetType() == typeof(ContentItemModelWithAttributes))); } + [Fact] public void GetStronglyTypedItemsFeed_DepthParameter_ThrowsArgumentException() @@ -1038,6 +1039,20 @@ public async Task CastResponse() Assert.Equal("Text field value", response.Item.TextField); } + [Fact] + public async Task DynamicResponse() + { + _mockHttp + .When($"{_baseUrl}/items/complete_content_item") + .Respond("application/json", await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}complete_content_item.json"))); + + var client = InitializeDeliveryClientWithCustomModelProvider(_mockHttp, new PropertyMapper());// , new CustomTypeProvider()); + + var response = await client.GetItemAsync("complete_content_item"); + + Assert.NotEmpty(response.Item.Elements); + } + [Fact] public async Task CastListingResponse() { diff --git a/Kontent.Ai.Delivery.Tests/Models/DynamicContentItemModel.cs b/Kontent.Ai.Delivery.Tests/Models/DynamicContentItemModel.cs new file mode 100644 index 00000000..67dbe4d9 --- /dev/null +++ b/Kontent.Ai.Delivery.Tests/Models/DynamicContentItemModel.cs @@ -0,0 +1,10 @@ +using Kontent.Ai.Delivery.Abstractions; +namespace Kontent.Ai.Delivery.Tests.Models +{ + public class DynamicContentItemModel + { + public IContentItemSystemAttributes System { get; set; } + + public IElements Elements { get; set; } + } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs index 5d176f76..aaa6521c 100644 --- a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs @@ -106,6 +106,16 @@ internal async Task GetContentItemModelAsync(Type modelType, JToken seri property.SetValue(instance, itemSystemAttributes); } } + else if (typeof(IElements).IsAssignableFrom(property.PropertyType)) + { + + var value = await GetAllPropertiesValuesAsync(elementsData, property, linkedItems, context, itemSystemAttributes, processedItems, richTextPropertiesToBeProcessed); + if (value != null) + { + property.SetValue(instance, value); + } + + } else { var value = await GetPropertyValueAsync(elementsData, property, linkedItems, context, itemSystemAttributes, processedItems, richTextPropertiesToBeProcessed); @@ -211,6 +221,34 @@ async Task GetLinkedItemAsync(string codename) }; } + private async Task GetAllPropertiesValuesAsync(JObject elementsData, PropertyInfo property, JObject linkedItems, ResolvingContext context, IContentItemSystemAttributes itemSystemAttributes, Dictionary processedItems, List richTextPropertiesToBeProcessed) + { + var result = new IElements(); + foreach (var item in elementsData) + { + var key = item.Key; + var elementValue = (JObject)item.Value; + // var elementValue = elementsData?.Properties()?.Select(p => (p.Name, (JObject)p.Value)).FirstOrDefault(); + // TODO think about value converter + var value = (elementValue["type"].ToString()) switch + { + // TODO do we want to use string/structured data for rich text + // "rich_text" => await GetPropertyValueAsync(elementsData, null, linkedItems, context, itemSystemAttributes, processedItems, richTextPropertiesToBeProcessed); + // "asset" => await GetElementModelAsync>(property, context, elementValue, null), + // "number" => await GetElementModelAsync, decimal?>(property, context, elementValue, null), + // TODO do we want to use string/structured data for date time + // "date_time" => await GetElementModelAsync(property, context, elementValue, null), + // "multiple_choice" => await GetElementModelAsync>, List>(property, context, elementValue, null), + // "taxonomy" => await GetElementModelAsync>(property, context, elementValue, null), + // "modular_content" => await GetElementModelAsync>, List>(property, context, elementValue, null), + // Custom element, text element, URL slug element + _ => GetRawValue(elementValue)?.ToString() + }; + // TODO implement + result.Add(key, value); + } + return result; + } private async Task GetPropertyValueAsync(JObject elementsData, PropertyInfo property, JObject linkedItems, ResolvingContext context, IContentItemSystemAttributes itemSystemAttributes, Dictionary processedItems, List richTextPropertiesToBeProcessed) { var elementDefinition = GetElementData(elementsData, property, itemSystemAttributes); @@ -389,7 +427,7 @@ private IPropertyValueConverter GetValueConverter(PropertyInfo property) { return new AssetElementValueConverter(DeliveryOptions); } - + return null; } From ee6a143f0b468f28224eadfc9484a8ece8efe92e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Tue, 28 Feb 2023 15:45:20 +0100 Subject: [PATCH 02/34] implement provider for IElements with just elements values (not the whole object) --- .../ContentItems/ModelProvider.cs | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs index aaa6521c..51c6c59b 100644 --- a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs @@ -109,7 +109,7 @@ internal async Task GetContentItemModelAsync(Type modelType, JToken seri else if (typeof(IElements).IsAssignableFrom(property.PropertyType)) { - var value = await GetAllPropertiesValuesAsync(elementsData, property, linkedItems, context, itemSystemAttributes, processedItems, richTextPropertiesToBeProcessed); + var value = GetAllPropertiesValuesAsync(elementsData, property, linkedItems, context, itemSystemAttributes, processedItems, richTextPropertiesToBeProcessed); if (value != null) { property.SetValue(instance, value); @@ -221,30 +221,32 @@ async Task GetLinkedItemAsync(string codename) }; } - private async Task GetAllPropertiesValuesAsync(JObject elementsData, PropertyInfo property, JObject linkedItems, ResolvingContext context, IContentItemSystemAttributes itemSystemAttributes, Dictionary processedItems, List richTextPropertiesToBeProcessed) + private IElements GetAllPropertiesValuesAsync(JObject elementsData, PropertyInfo property, JObject linkedItems, ResolvingContext context, IContentItemSystemAttributes itemSystemAttributes, Dictionary processedItems, List richTextPropertiesToBeProcessed) { var result = new IElements(); foreach (var item in elementsData) { var key = item.Key; - var elementValue = (JObject)item.Value; - // var elementValue = elementsData?.Properties()?.Select(p => (p.Name, (JObject)p.Value)).FirstOrDefault(); - // TODO think about value converter - var value = (elementValue["type"].ToString()) switch + var element = (JObject)item.Value; + var elementValue = element["value"]; + + // TODO think about value converter implementation + + object value = (element["type"].ToString()) switch { // TODO do we want to use string/structured data for rich text - // "rich_text" => await GetPropertyValueAsync(elementsData, null, linkedItems, context, itemSystemAttributes, processedItems, richTextPropertiesToBeProcessed); - // "asset" => await GetElementModelAsync>(property, context, elementValue, null), - // "number" => await GetElementModelAsync, decimal?>(property, context, elementValue, null), + "rich_text" => elementValue.ToObject(Serializer), + "asset" => elementValue.ToObject>(Serializer), + "number" => elementValue.ToObject(Serializer), // TODO do we want to use string/structured data for date time - // "date_time" => await GetElementModelAsync(property, context, elementValue, null), - // "multiple_choice" => await GetElementModelAsync>, List>(property, context, elementValue, null), - // "taxonomy" => await GetElementModelAsync>(property, context, elementValue, null), - // "modular_content" => await GetElementModelAsync>, List>(property, context, elementValue, null), + "date_time" => elementValue.ToObject(Serializer), + "multiple_choice" => elementValue.ToObject>(Serializer), + "taxonomy" => elementValue.ToObject>(Serializer), + "modular_content" => elementValue.ToObject>(Serializer), // Custom element, text element, URL slug element - _ => GetRawValue(elementValue)?.ToString() + _ => elementValue.ToObject(Serializer) }; - // TODO implement + result.Add(key, value); } return result; From 7824c1faad831bf20efdefbf23252ac312f7ac7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Tue, 28 Feb 2023 16:01:02 +0100 Subject: [PATCH 03/34] rename the base calss for content items --- .../ContentItems/Elements/ContentItemElements.cs | 14 ++++++++++++++ .../ContentItems/Elements/IElements.cs | 11 ----------- .../Models/DynamicContentItemModel.cs | 2 +- Kontent.Ai.Delivery/ContentItems/ModelProvider.cs | 6 +++--- 4 files changed, 18 insertions(+), 15 deletions(-) create mode 100644 Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs delete mode 100644 Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IElements.cs diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs new file mode 100644 index 00000000..1c37477d --- /dev/null +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Kontent.Ai.Delivery.Abstractions +{ + // TODO validate this to get whole element context + /// + /// Dynamic dictionary of strongly type elements for dynamic item fetch. + /// Values are based on + /// + public class ContentItemElements : Dictionary + { + + } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IElements.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IElements.cs deleted file mode 100644 index 03d1f9f5..00000000 --- a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IElements.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; - -namespace Kontent.Ai.Delivery.Abstractions -{ - // TODO validate this to get whole element context - // public class IElements : Dictionary> - public class IElements : Dictionary - { - - } -} \ No newline at end of file diff --git a/Kontent.Ai.Delivery.Tests/Models/DynamicContentItemModel.cs b/Kontent.Ai.Delivery.Tests/Models/DynamicContentItemModel.cs index 67dbe4d9..1600bf4e 100644 --- a/Kontent.Ai.Delivery.Tests/Models/DynamicContentItemModel.cs +++ b/Kontent.Ai.Delivery.Tests/Models/DynamicContentItemModel.cs @@ -5,6 +5,6 @@ public class DynamicContentItemModel { public IContentItemSystemAttributes System { get; set; } - public IElements Elements { get; set; } + public ContentItemElements Elements { get; set; } } } \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs index 51c6c59b..a4a433c4 100644 --- a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs @@ -106,7 +106,7 @@ internal async Task GetContentItemModelAsync(Type modelType, JToken seri property.SetValue(instance, itemSystemAttributes); } } - else if (typeof(IElements).IsAssignableFrom(property.PropertyType)) + else if (typeof(ContentItemElements).IsAssignableFrom(property.PropertyType)) { var value = GetAllPropertiesValuesAsync(elementsData, property, linkedItems, context, itemSystemAttributes, processedItems, richTextPropertiesToBeProcessed); @@ -221,9 +221,9 @@ async Task GetLinkedItemAsync(string codename) }; } - private IElements GetAllPropertiesValuesAsync(JObject elementsData, PropertyInfo property, JObject linkedItems, ResolvingContext context, IContentItemSystemAttributes itemSystemAttributes, Dictionary processedItems, List richTextPropertiesToBeProcessed) + private ContentItemElements GetAllPropertiesValuesAsync(JObject elementsData, PropertyInfo property, JObject linkedItems, ResolvingContext context, IContentItemSystemAttributes itemSystemAttributes, Dictionary processedItems, List richTextPropertiesToBeProcessed) { - var result = new IElements(); + var result = new ContentItemElements(); foreach (var item in elementsData) { var key = item.Key; From 75047ca992db4423112cecc1c545ed96e4e5534b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Tue, 28 Feb 2023 16:28:46 +0100 Subject: [PATCH 04/34] trial to wrap dynamic elements --- .../Elements/ContentItemElements.cs | 2 +- .../Elements/DecimalElementValue.cs | 8 +++++++ .../Elements/StringElementValue.cs | 6 +++++ .../ContentItems/ModelProvider.cs | 23 ++++++++++--------- 4 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 Kontent.Ai.Delivery/ContentItems/Elements/DecimalElementValue.cs create mode 100644 Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs index 1c37477d..62026d11 100644 --- a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs @@ -7,7 +7,7 @@ namespace Kontent.Ai.Delivery.Abstractions /// Dynamic dictionary of strongly type elements for dynamic item fetch. /// Values are based on /// - public class ContentItemElements : Dictionary + public class ContentItemElements : Dictionary> { } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/DecimalElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/DecimalElementValue.cs new file mode 100644 index 00000000..7609a62d --- /dev/null +++ b/Kontent.Ai.Delivery/ContentItems/Elements/DecimalElementValue.cs @@ -0,0 +1,8 @@ +using System; + +namespace Kontent.Ai.Delivery.ContentItems.Elements +{ + internal class DecimalElementValue: ContentElementValue + { + } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs new file mode 100644 index 00000000..68f35928 --- /dev/null +++ b/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs @@ -0,0 +1,6 @@ +namespace Kontent.Ai.Delivery.ContentItems.Elements +{ + internal class StringElementValue : ContentElementValue + { + } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs index a4a433c4..933fdba0 100644 --- a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs @@ -228,23 +228,24 @@ private ContentItemElements GetAllPropertiesValuesAsync(JObject elementsData, Pr { var key = item.Key; var element = (JObject)item.Value; - var elementValue = element["value"]; - // TODO think about value converter implementation + var x = element.ToObject(Serializer); - object value = (element["type"].ToString()) switch + // TODO think about value converter implementation + // TODO what about codename property - now it is null + IContentElementValue value = (element["type"].ToString()) switch { // TODO do we want to use string/structured data for rich text - "rich_text" => elementValue.ToObject(Serializer), - "asset" => elementValue.ToObject>(Serializer), - "number" => elementValue.ToObject(Serializer), + "rich_text" => element.ToObject(Serializer), + "asset" => element.ToObject(Serializer), + "number" => element.ToObject(Serializer), // TODO do we want to use string/structured data for date time - "date_time" => elementValue.ToObject(Serializer), - "multiple_choice" => elementValue.ToObject>(Serializer), - "taxonomy" => elementValue.ToObject>(Serializer), - "modular_content" => elementValue.ToObject>(Serializer), + "date_time" => element.ToObject(Serializer), + "multiple_choice" => element.ToObject>>(Serializer), + "taxonomy" => element.ToObject>>(Serializer), + "modular_content" => element.ToObject>>(Serializer), // Custom element, text element, URL slug element - _ => elementValue.ToObject(Serializer) + _ => element.ToObject(Serializer) }; result.Add(key, value); From cb405e0f759a9167c0ecc7b935220c3a17e99567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Tue, 28 Feb 2023 17:39:11 +0100 Subject: [PATCH 05/34] fix the class vs. struct interoperability --- .../Elements/ContentItemElements.cs | 4 +- .../DeliveryClientTests.cs | 89 ++++++++++++++----- ...lElementValue.cs => NumberElementValue.cs} | 2 +- .../ContentItems/ModelProvider.cs | 17 ++-- 4 files changed, 76 insertions(+), 36 deletions(-) rename Kontent.Ai.Delivery/ContentItems/Elements/{DecimalElementValue.cs => NumberElementValue.cs} (53%) diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs index 62026d11..cb0ab619 100644 --- a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs @@ -7,8 +7,8 @@ namespace Kontent.Ai.Delivery.Abstractions /// Dynamic dictionary of strongly type elements for dynamic item fetch. /// Values are based on /// - public class ContentItemElements : Dictionary> + public class ContentItemElements : Dictionary { } -} \ No newline at end of file +} diff --git a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs index 5a11ce25..fda28d50 100644 --- a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs +++ b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs @@ -10,6 +10,7 @@ using Kontent.Ai.Delivery.Abstractions; using Kontent.Ai.Delivery.Builders.DeliveryClient; using Kontent.Ai.Delivery.ContentItems; +using Kontent.Ai.Delivery.ContentItems.Elements; using Kontent.Ai.Delivery.ContentItems.RichText.Blocks; using Kontent.Ai.Delivery.SharedModels; using Kontent.Ai.Delivery.Tests.Factories; @@ -195,7 +196,7 @@ public async Task GetItemAsync_NotFound_RespondsWithApiError() AssertErrorResponse(actualResponse, expectedError); Assert.Null(actualResponse.Item); } - + [Fact] public async Task GetItemAsync_InvalidProjectId_RespondsWithApiError() { @@ -209,7 +210,7 @@ public async Task GetItemAsync_InvalidProjectId_RespondsWithApiError() var client = InitializeDeliveryClientWithACustomTypeProvider(_mockHttp); var actualResponse = await client.GetItemAsync("coffee_beverages_explained"); - + AssertErrorResponse(actualResponse, expectedError); Assert.Null(actualResponse.Item); } @@ -289,7 +290,7 @@ public async Task GetItemsAsync() Assert.True(response?.ApiResponse?.IsSuccess); Assert.NotEmpty(response.Items); } - + [Fact] public async Task GetItemsAsync_InvalidProjectId_RespondsWithApiError() { @@ -303,12 +304,12 @@ public async Task GetItemsAsync_InvalidProjectId_RespondsWithApiError() var client = InitializeDeliveryClientWithACustomTypeProvider(_mockHttp); var actualResponse = await client.GetItemsAsync(); - + AssertErrorResponse(actualResponse, expectedError); Assert.Null(actualResponse.Items); Assert.Null(actualResponse.Pagination); } - + [Fact] public void GetItemsFeed_DepthParameter_ThrowsArgumentException() { @@ -414,7 +415,7 @@ public async Task GetItemsFeed_MultipleBatches_FetchNextBatchAsync() Assert.Equal(6, items.Count); Assert.Equal(2, timesCalled); } - + [Fact] public async Task GetItemsFeed_InvalidProjectId_RespondsWithApiError() { @@ -428,7 +429,7 @@ public async Task GetItemsFeed_InvalidProjectId_RespondsWithApiError() var client = InitializeDeliveryClientWithACustomTypeProvider(_mockHttp); var actualResponse = await client.GetItemsFeed().FetchNextBatchAsync(); - + AssertErrorResponse(actualResponse, expectedError); Assert.Null(actualResponse.Items); } @@ -478,7 +479,7 @@ public async Task GetTypeAsync() Assert.IsAssignableFrom(processingTaxonomyElement); Assert.Equal("processing", ((ITaxonomyElement)processingTaxonomyElement).TaxonomyGroup); } - + [Fact] public async Task GetTypeAsync_NotFound_RespondsWithApiError() { @@ -502,7 +503,7 @@ public async Task GetTypeAsync_NotFound_RespondsWithApiError() AssertErrorResponse(actualResponse, expectedError); Assert.Null(actualResponse.Type); } - + [Fact] public async Task GetTypeAsync_InvalidProjectId_RespondsWithApiError() { @@ -516,7 +517,7 @@ public async Task GetTypeAsync_InvalidProjectId_RespondsWithApiError() var client = InitializeDeliveryClientWithACustomTypeProvider(_mockHttp); var actualResponse = await client.GetTypeAsync("unequestrian_nonadjournment_sur_achoerodus"); - + AssertErrorResponse(actualResponse, expectedError); Assert.Null(actualResponse.Type); } @@ -691,7 +692,7 @@ public async Task GetTaxonomiesAsync() Assert.NotNull(response.ApiResponse.RequestUrl); Assert.NotEmpty(response.Taxonomies); } - + [Fact] public async Task GetTaxonomiesAsync_InvalidProjectId_RespondsWithApiError() { @@ -705,7 +706,7 @@ public async Task GetTaxonomiesAsync_InvalidProjectId_RespondsWithApiError() var client = InitializeDeliveryClientWithACustomTypeProvider(_mockHttp); var actualResponse = await client.GetTaxonomiesAsync(); - + AssertErrorResponse(actualResponse, expectedError); Assert.Null(actualResponse.Taxonomies); Assert.Null(actualResponse.Pagination); @@ -936,7 +937,7 @@ public async Task GetStronglyTypedItemsResponse() // Assert Assert.True(items.All(i => i.GetType() == typeof(ContentItemModelWithAttributes))); } - + [Fact] public void GetStronglyTypedItemsFeed_DepthParameter_ThrowsArgumentException() @@ -1050,7 +1051,47 @@ public async Task DynamicResponse() var response = await client.GetItemAsync("complete_content_item"); - Assert.NotEmpty(response.Item.Elements); + Assert.All(response.Item.Elements, item => + { + if (typeof(IContentElementValue).IsAssignableFrom(item.Value.GetType())) + { + var elementObject = (IContentElementValue)item.Value; + Assert.NotNull(elementObject.Value); + Assert.NotNull(elementObject.Type); + Assert.NotNull(elementObject.Name); + // TODO think about that one + Assert.Null(elementObject.Codename); + } + else if (typeof(DateTimeElementValue).IsAssignableFrom(item.Value.GetType())) + { + var elementObject = (DateTimeElementValue)item.Value; + Assert.NotNull(elementObject.Value); + Assert.NotNull(elementObject.Type); + Assert.NotNull(elementObject.Name); + // TODO think about that one + Assert.Null(elementObject.Codename); + + } + else if (typeof(NumberElementValue).IsAssignableFrom(item.Value.GetType())) + { + var elementObject = (NumberElementValue)item.Value; + Assert.NotNull(elementObject.Value); + Assert.NotNull(elementObject.Type); + Assert.NotNull(elementObject.Name); + // TODO think about that one + Assert.Null(elementObject.Codename); + } + }); + Assert.All(response.Item.Elements.OfType(), item => + { + Assert.NotNull(item.Images); + Assert.NotNull(item.Links); + Assert.NotNull(item.ModularContent); + }); + Assert.All(response.Item.Elements.OfType(), item => + { + Assert.NotNull(item.TaxonomyGroup); + }); } [Fact] @@ -1491,7 +1532,7 @@ public async Task GetTypesAsync_ApiDoesNotReturnStaleContent_ResponseDoesNotIndi Assert.False(response.ApiResponse.HasStaleContent); Assert.True(response.Types.Any()); } - + [Fact] public async Task GetTypesAsync_InvalidProjectId_RespondsWithApiError() { @@ -1505,7 +1546,7 @@ public async Task GetTypesAsync_InvalidProjectId_RespondsWithApiError() var client = InitializeDeliveryClientWithACustomTypeProvider(_mockHttp); var actualResponse = await client.GetTypesAsync(); - + AssertErrorResponse(actualResponse, expectedError); Assert.Null(actualResponse.Types); Assert.Null(actualResponse.Pagination); @@ -1636,7 +1677,7 @@ public async Task GetLanguagesAsync_ApiDoesNotReturnStaleContent_ResponseDoesNot Assert.False(response.ApiResponse.HasStaleContent); Assert.True(response.Languages.Any()); } - + [Fact] public async Task GetLanguagesAsync_InvalidProjectId_RespondsWithApiError() { @@ -1650,7 +1691,7 @@ public async Task GetLanguagesAsync_InvalidProjectId_RespondsWithApiError() var client = InitializeDeliveryClientWithACustomTypeProvider(_mockHttp); var actualResponse = await client.GetLanguagesAsync(); - + AssertErrorResponse(actualResponse, expectedError); Assert.Null(actualResponse.Languages); Assert.Null(actualResponse.Pagination); @@ -1695,7 +1736,7 @@ public async Task GetElementAsync_ApiDoesNotReturnStaleContent_ResponseDoesNotIn Assert.False(response.ApiResponse.HasStaleContent); Assert.NotNull(response.Element.Codename); } - + [Fact] public async Task GetElementAsync_InvalidProjectId_RespondsWithApiError() { @@ -1709,7 +1750,7 @@ public async Task GetElementAsync_InvalidProjectId_RespondsWithApiError() var client = InitializeDeliveryClientWithACustomTypeProvider(_mockHttp); var actualResponse = await client.GetContentElementAsync("test", "test"); - + AssertErrorResponse(actualResponse, expectedError); Assert.Null(actualResponse.Element); } @@ -1808,7 +1849,7 @@ public async Task RetrieveContentItem_GetLinkedItems_TypeItemsManually() ); var items = (await Task.WhenAll(itemTasks)).ToList(); Assert.Equal(2, items.Count()); - + var tweetItem = items[0]; var hostedVideoItem = items[1]; Assert.Equal(tweetItem.GetType(), typeof(Tweet)); @@ -1859,8 +1900,8 @@ private DeliveryClient InitializeDeliveryClientWithCustomModelProvider(MockHttpM return client; } - - private Error CreateInvalidProjectIdApiError() => new Error() + + private Error CreateInvalidProjectIdApiError() => new Error() { ErrorCode = 105, RequestId = "", @@ -1874,7 +1915,7 @@ private static string CreateApiErrorResponse(Error error) private static void AssertErrorResponse(IResponse actualResponse, IError expectedError) { var actualError = actualResponse.ApiResponse.Error; - + Assert.NotNull(actualResponse.ApiResponse.Error); Assert.False(actualResponse.ApiResponse.IsSuccess); Assert.Equal(expectedError.Message, actualError.Message); diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/DecimalElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/NumberElementValue.cs similarity index 53% rename from Kontent.Ai.Delivery/ContentItems/Elements/DecimalElementValue.cs rename to Kontent.Ai.Delivery/ContentItems/Elements/NumberElementValue.cs index 7609a62d..a55b0328 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/DecimalElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/NumberElementValue.cs @@ -2,7 +2,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - internal class DecimalElementValue: ContentElementValue + internal class NumberElementValue: ContentElementValue { } } \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs index 933fdba0..03a4fdc4 100644 --- a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs @@ -227,27 +227,26 @@ private ContentItemElements GetAllPropertiesValuesAsync(JObject elementsData, Pr foreach (var item in elementsData) { var key = item.Key; - var element = (JObject)item.Value; - - var x = element.ToObject(Serializer); + var element = item.Value; // TODO think about value converter implementation // TODO what about codename property - now it is null - IContentElementValue value = (element["type"].ToString()) switch + object value = (element["type"].ToString()) switch { // TODO do we want to use string/structured data for rich text - "rich_text" => element.ToObject(Serializer), + "rich_text" => element.ToObject(Serializer), "asset" => element.ToObject(Serializer), - "number" => element.ToObject(Serializer), + "number" => element.ToObject(Serializer), // TODO do we want to use string/structured data for date time "date_time" => element.ToObject(Serializer), - "multiple_choice" => element.ToObject>>(Serializer), - "taxonomy" => element.ToObject>>(Serializer), - "modular_content" => element.ToObject>>(Serializer), + "multiple_choice" => element.ToObject>>(Serializer), + "taxonomy" => element.ToObject>>(Serializer), + "modular_content" => element.ToObject>>(Serializer), // Custom element, text element, URL slug element _ => element.ToObject(Serializer) }; + // TODO Fix the empty Codename? result.Add(key, value); } return result; From 2c6b5e9ddac4e4ad8bae852a995dbf7d7537d7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Mon, 6 Mar 2023 17:39:57 +0100 Subject: [PATCH 06/34] inject another interface to base the element witout generic + more TODOs --- .../Elements/ContentItemElements.cs | 2 +- .../Elements/IContentElementValue.cs | 13 ++++++++-- .../DeliveryClientTests.cs | 25 +++++++++++++++++++ .../Elements/RichTextElementValue.cs | 2 +- .../ContentItems/ModelProvider.cs | 8 +++--- 5 files changed, 43 insertions(+), 7 deletions(-) diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs index cb0ab619..a0b0c268 100644 --- a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs @@ -7,7 +7,7 @@ namespace Kontent.Ai.Delivery.Abstractions /// Dynamic dictionary of strongly type elements for dynamic item fetch. /// Values are based on /// - public class ContentItemElements : Dictionary + public class ContentItemElements : Dictionary { } diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IContentElementValue.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IContentElementValue.cs index a2abf2e5..1a033149 100644 --- a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IContentElementValue.cs +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IContentElementValue.cs @@ -1,15 +1,24 @@ namespace Kontent.Ai.Delivery.Abstractions { + // TODO think whether the IContentElementValue is needed /// - /// Represents a content element. + /// Represents a generic content element with value. /// - public interface IContentElementValue + public interface IContentElementValue : IContentElementValue { /// /// Gets the value of the content element. /// T Value { get; } + + } + + /// + /// Represents a content element. + /// + public interface IContentElementValue + { /// /// Gets the codename of the content element. /// diff --git a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs index fda28d50..2026573c 100644 --- a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs +++ b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs @@ -1053,6 +1053,31 @@ public async Task DynamicResponse() Assert.All(response.Item.Elements, item => { + + + // Assignment with pattern matching + var x = item.Value switch + { + AssetElementValue y => y.Codename, + _ => null + }; + + switch (item.Value.Type) + { + case "rich_text": + + break; + case "modular_content": + // TODO extract ContentElementValue> as separate type LinkedItems(SubPages)ElementValue + var linkedItems = (ContentElementValue>)item.Value; + // TODO check out how to work with linked items => for tree traversal the strongly typed way + // response.ApiResponse.getLinkedItem(linkedItems.Value[0]); + break; + default: + break; + } + + // if (typeof(IContentElementValue).IsAssignableFrom(item.Value.GetType())) { var elementObject = (IContentElementValue)item.Value; diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/RichTextElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/RichTextElementValue.cs index 6d03f898..da7da5b9 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/RichTextElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/RichTextElementValue.cs @@ -5,7 +5,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - internal class RichTextElementValue : ContentElementValue, IRichTextElementValue + internal class RichTextElementValue : StringElementValue, IRichTextElementValue { [JsonProperty("images")] public IDictionary Images { get; set; } diff --git a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs index 03a4fdc4..e406a5b2 100644 --- a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs @@ -231,9 +231,9 @@ private ContentItemElements GetAllPropertiesValuesAsync(JObject elementsData, Pr // TODO think about value converter implementation // TODO what about codename property - now it is null - object value = (element["type"].ToString()) switch + IContentElementValue value = (element["type"].ToString()) switch { - // TODO do we want to use string/structured data for rich text + // TODO do we want to use string/structured data for rich text - probably think about support both ways "rich_text" => element.ToObject(Serializer), "asset" => element.ToObject(Serializer), "number" => element.ToObject(Serializer), @@ -242,11 +242,13 @@ private ContentItemElements GetAllPropertiesValuesAsync(JObject elementsData, Pr "multiple_choice" => element.ToObject>>(Serializer), "taxonomy" => element.ToObject>>(Serializer), "modular_content" => element.ToObject>>(Serializer), + // TODO do we need to split this into UrlSlugElementValue/CustomElementValue/TextElementValue => Custom value has Searchable value => split to more classes // Custom element, text element, URL slug element _ => element.ToObject(Serializer) }; - // TODO Fix the empty Codename? + // TODO Fix the empty Codename? Probably yes (wrap ToObject with normalization logic) + // value.Codename = key; result.Add(key, value); } return result; From fee47426d113d3a4d5b8454f39511d3d8ac11037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Mon, 6 Mar 2023 18:32:59 +0100 Subject: [PATCH 07/34] class per element value --- .../ContentItems/Elements/CustomElementValue.cs | 7 +++++++ .../Elements/MultipleChoiceElementValue.cs | 10 ++++++++++ .../ContentItems/Elements/StringElementValue.cs | 2 +- .../ContentItems/Elements/TextElementValue.cs | 7 +++++++ .../ContentItems/Elements/UrlSlugElementValue.cs | 7 +++++++ Kontent.Ai.Delivery/ContentItems/ModelProvider.cs | 14 ++++++++------ 6 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 Kontent.Ai.Delivery/ContentItems/Elements/CustomElementValue.cs create mode 100644 Kontent.Ai.Delivery/ContentItems/Elements/MultipleChoiceElementValue.cs create mode 100644 Kontent.Ai.Delivery/ContentItems/Elements/TextElementValue.cs create mode 100644 Kontent.Ai.Delivery/ContentItems/Elements/UrlSlugElementValue.cs diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/CustomElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/CustomElementValue.cs new file mode 100644 index 00000000..2055240a --- /dev/null +++ b/Kontent.Ai.Delivery/ContentItems/Elements/CustomElementValue.cs @@ -0,0 +1,7 @@ +namespace Kontent.Ai.Delivery.ContentItems.Elements +{ + internal class CustomElementValue : StringElementValue + { + + } +} diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/MultipleChoiceElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/MultipleChoiceElementValue.cs new file mode 100644 index 00000000..a1cb92e3 --- /dev/null +++ b/Kontent.Ai.Delivery/ContentItems/Elements/MultipleChoiceElementValue.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Kontent.Ai.Delivery.Abstractions; + +namespace Kontent.Ai.Delivery.ContentItems.Elements +{ + internal class MultipleChoiceElementValue: ContentElementValue> + { + + } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs index 68f35928..f60b40f5 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs @@ -1,6 +1,6 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - internal class StringElementValue : ContentElementValue + internal abstract class StringElementValue : ContentElementValue { } } \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/TextElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/TextElementValue.cs new file mode 100644 index 00000000..efc3addb --- /dev/null +++ b/Kontent.Ai.Delivery/ContentItems/Elements/TextElementValue.cs @@ -0,0 +1,7 @@ +namespace Kontent.Ai.Delivery.ContentItems.Elements +{ + internal class TextElementValue : StringElementValue + { + + } +} diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/UrlSlugElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/UrlSlugElementValue.cs new file mode 100644 index 00000000..f132304c --- /dev/null +++ b/Kontent.Ai.Delivery/ContentItems/Elements/UrlSlugElementValue.cs @@ -0,0 +1,7 @@ +namespace Kontent.Ai.Delivery.ContentItems.Elements +{ + internal class UrlSlugElementValue : StringElementValue + { + + } +} diff --git a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs index e406a5b2..9d915b43 100644 --- a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs @@ -237,14 +237,16 @@ private ContentItemElements GetAllPropertiesValuesAsync(JObject elementsData, Pr "rich_text" => element.ToObject(Serializer), "asset" => element.ToObject(Serializer), "number" => element.ToObject(Serializer), - // TODO do we want to use string/structured data for date time + // TODO do we want to use string/structured data for date time => structured is OK "date_time" => element.ToObject(Serializer), - "multiple_choice" => element.ToObject>>(Serializer), - "taxonomy" => element.ToObject>>(Serializer), + "multiple_choice" => element.ToObject(Serializer), + "taxonomy" => element.ToObject(Serializer), + // TODO what Linked items + what SubPages? "modular_content" => element.ToObject>>(Serializer), - // TODO do we need to split this into UrlSlugElementValue/CustomElementValue/TextElementValue => Custom value has Searchable value => split to more classes - // Custom element, text element, URL slug element - _ => element.ToObject(Serializer) + "custom_element" => element.ToObject(Serializer), + "url_slug" => element.ToObject(Serializer), + "text" => element.ToObject(Serializer), + _ => throw new ArgumentException($"Argument type {element["type"].ToString()} not supported.") }; // TODO Fix the empty Codename? Probably yes (wrap ToObject with normalization logic) From 876f83eeda7d488642bb4b4d30169ce011ca09de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Mon, 6 Mar 2023 18:41:17 +0100 Subject: [PATCH 08/34] unify codename for content elements --- .../Elements/IContentElementValue.cs | 2 -- .../DeliveryClientTests.cs | 9 +++++--- .../Elements/ContentElementValue.cs | 8 +++++++ .../ContentItems/ModelProvider.cs | 23 ++++++++++--------- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IContentElementValue.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IContentElementValue.cs index 1a033149..e797948c 100644 --- a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IContentElementValue.cs +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IContentElementValue.cs @@ -10,8 +10,6 @@ public interface IContentElementValue : IContentElementValue /// Gets the value of the content element. /// T Value { get; } - - } /// diff --git a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs index 2026573c..05518834 100644 --- a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs +++ b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs @@ -1085,7 +1085,8 @@ public async Task DynamicResponse() Assert.NotNull(elementObject.Type); Assert.NotNull(elementObject.Name); // TODO think about that one - Assert.Null(elementObject.Codename); + // Assert.Null(elementObject.Codename); + Assert.NotNull(elementObject.Codename); } else if (typeof(DateTimeElementValue).IsAssignableFrom(item.Value.GetType())) { @@ -1094,7 +1095,8 @@ public async Task DynamicResponse() Assert.NotNull(elementObject.Type); Assert.NotNull(elementObject.Name); // TODO think about that one - Assert.Null(elementObject.Codename); + // Assert.Null(elementObject.Codename); + Assert.NotNull(elementObject.Codename); } else if (typeof(NumberElementValue).IsAssignableFrom(item.Value.GetType())) @@ -1104,7 +1106,8 @@ public async Task DynamicResponse() Assert.NotNull(elementObject.Type); Assert.NotNull(elementObject.Name); // TODO think about that one - Assert.Null(elementObject.Codename); + // Assert.Null(elementObject.Codename); + Assert.NotNull(elementObject.Codename); } }); Assert.All(response.Item.Elements.OfType(), item => diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/ContentElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/ContentElementValue.cs index 90352591..7731114b 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/ContentElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/ContentElementValue.cs @@ -20,5 +20,13 @@ internal class ContentElementValue : IContentElementValue /// [JsonProperty("codename")] public string Codename { get; internal set; } + + // TODO is that a good idea? + public ContentElementValue WithCodename(string codename) + { + Codename = codename; + return this; + } + } } diff --git a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs index 9d915b43..c7b10b5e 100644 --- a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs @@ -234,19 +234,19 @@ private ContentItemElements GetAllPropertiesValuesAsync(JObject elementsData, Pr IContentElementValue value = (element["type"].ToString()) switch { // TODO do we want to use string/structured data for rich text - probably think about support both ways - "rich_text" => element.ToObject(Serializer), - "asset" => element.ToObject(Serializer), - "number" => element.ToObject(Serializer), + "rich_text" => element.ToObject(Serializer).WithCodename(key), + "asset" => element.ToObject(Serializer).WithCodename(key), + "number" => element.ToObject(Serializer).WithCodename(key), // TODO do we want to use string/structured data for date time => structured is OK - "date_time" => element.ToObject(Serializer), - "multiple_choice" => element.ToObject(Serializer), - "taxonomy" => element.ToObject(Serializer), + "date_time" => element.ToObject(Serializer).WithCodename(key), + "multiple_choice" => element.ToObject(Serializer).WithCodename(key), + "taxonomy" => element.ToObject(Serializer).WithCodename(key), // TODO what Linked items + what SubPages? - "modular_content" => element.ToObject>>(Serializer), - "custom_element" => element.ToObject(Serializer), - "url_slug" => element.ToObject(Serializer), - "text" => element.ToObject(Serializer), - _ => throw new ArgumentException($"Argument type {element["type"].ToString()} not supported.") + "modular_content" => element.ToObject>>(Serializer).WithCodename(key), + "custom" => element.ToObject(Serializer).WithCodename(key), + "url_slug" => element.ToObject(Serializer).WithCodename(key), + "text" => element.ToObject(Serializer).WithCodename(key), + _ => throw new ArgumentException($"Argument type ({element["type"].ToString()}) not supported.") }; // TODO Fix the empty Codename? Probably yes (wrap ToObject with normalization logic) @@ -255,6 +255,7 @@ private ContentItemElements GetAllPropertiesValuesAsync(JObject elementsData, Pr } return result; } + private async Task GetPropertyValueAsync(JObject elementsData, PropertyInfo property, JObject linkedItems, ResolvingContext context, IContentItemSystemAttributes itemSystemAttributes, Dictionary processedItems, List richTextPropertiesToBeProcessed) { var elementDefinition = GetElementData(elementsData, property, itemSystemAttributes); From aeab7fa73bd2430dfe555764b363aa05e07091fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Mon, 6 Mar 2023 18:47:13 +0100 Subject: [PATCH 09/34] cleanup --- Kontent.Ai.Delivery/ContentItems/ModelProvider.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs index c7b10b5e..30f725d1 100644 --- a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs @@ -230,7 +230,7 @@ private ContentItemElements GetAllPropertiesValuesAsync(JObject elementsData, Pr var element = item.Value; // TODO think about value converter implementation - // TODO what about codename property - now it is null + // TODO what about codename property - now it is not null, bit withCodename is not really nice IContentElementValue value = (element["type"].ToString()) switch { // TODO do we want to use string/structured data for rich text - probably think about support both ways @@ -248,9 +248,6 @@ private ContentItemElements GetAllPropertiesValuesAsync(JObject elementsData, Pr "text" => element.ToObject(Serializer).WithCodename(key), _ => throw new ArgumentException($"Argument type ({element["type"].ToString()}) not supported.") }; - - // TODO Fix the empty Codename? Probably yes (wrap ToObject with normalization logic) - // value.Codename = key; result.Add(key, value); } return result; From aa257ead18bb708d4f86db663b6fff4f27297d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Tue, 28 Mar 2023 16:30:09 +0200 Subject: [PATCH 10/34] extract to separate logic --- .gitignore | 2 +- ...onverter.cs => IPropertyValueConverter.cs} | 0 .../IDeliveryUniversalItemListingResponse.cs | 16 +++++ .../IDeliveryUniversalItemResponse.cs | 17 +++++ .../IUniversalContentItem.cs} | 7 +- .../Universal/IUniversalModelProvider.cs | 12 ++++ .../IDeliveryClient.cs | 7 ++ .../DeliveryClientCache.cs | 12 ++++ .../DeliveryClientTests.cs | 11 ++-- .../Factories/DeliveryClientFactory.cs | 7 +- .../Models/DynamicContentItemModel.cs | 10 --- .../ValueConverterTests.cs | 14 ++-- .../ContentItems/ModelProvider.cs | 47 ------------- .../Universal/DeliveryItemGenericResponse.cs | 24 +++++++ .../Universal/GenericModelProvider.cs | 59 +++++++++++++++++ .../Universal/UniversalContentItem.cs | 11 ++++ Kontent.Ai.Delivery/DeliveryClient.cs | 66 +++++++++++++++---- .../structured-models/resolving-item-links.md | 2 +- 18 files changed, 238 insertions(+), 86 deletions(-) rename Kontent.Ai.Delivery.Abstractions/ContentItems/{IIPropertyValueConverter.cs => IPropertyValueConverter.cs} (100%) create mode 100644 Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IDeliveryUniversalItemListingResponse.cs create mode 100644 Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IDeliveryUniversalItemResponse.cs rename Kontent.Ai.Delivery.Abstractions/ContentItems/{Elements/ContentItemElements.cs => Universal/IUniversalContentItem.cs} (55%) create mode 100644 Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs delete mode 100644 Kontent.Ai.Delivery.Tests/Models/DynamicContentItemModel.cs create mode 100644 Kontent.Ai.Delivery/ContentItems/Universal/DeliveryItemGenericResponse.cs create mode 100644 Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs create mode 100644 Kontent.Ai.Delivery/ContentItems/Universal/UniversalContentItem.cs diff --git a/.gitignore b/.gitignore index 87771129..ffdfbc4a 100644 --- a/.gitignore +++ b/.gitignore @@ -141,7 +141,7 @@ publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings +# INFO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/IIPropertyValueConverter.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/IPropertyValueConverter.cs similarity index 100% rename from Kontent.Ai.Delivery.Abstractions/ContentItems/IIPropertyValueConverter.cs rename to Kontent.Ai.Delivery.Abstractions/ContentItems/IPropertyValueConverter.cs diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IDeliveryUniversalItemListingResponse.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IDeliveryUniversalItemListingResponse.cs new file mode 100644 index 00000000..b7a6b505 --- /dev/null +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IDeliveryUniversalItemListingResponse.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Kontent.Ai.Delivery.Abstractions +{ + public interface IDeliveryUniversalItemListingResponse : IResponse, IPageable + { + /// + /// Gets the content item. + /// + // TODO why it is IList + IList Items { get; } + + + Dictionary LinkedItems { get; } + } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IDeliveryUniversalItemResponse.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IDeliveryUniversalItemResponse.cs new file mode 100644 index 00000000..f4aee747 --- /dev/null +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IDeliveryUniversalItemResponse.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Kontent.Ai.Delivery.Abstractions +{ + /// + /// Represents a response from Kontent.ai Delivery API that contains a content item. + /// + public interface IDeliveryUniversalItemResponse: IDeliveryItemResponse, IResponse + { + /// + /// Gets the content item. + /// + IUniversalContentItem Item { get; } + + Dictionary LinkedItems { get; } + } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalContentItem.cs similarity index 55% rename from Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs rename to Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalContentItem.cs index a0b0c268..d42af0a4 100644 --- a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/ContentItemElements.cs +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalContentItem.cs @@ -4,11 +4,12 @@ namespace Kontent.Ai.Delivery.Abstractions { // TODO validate this to get whole element context /// - /// Dynamic dictionary of strongly type elements for dynamic item fetch. + /// Dynamic representation of the content item dynamic item processing. /// Values are based on /// - public class ContentItemElements : Dictionary + public interface IUniversalContentItem : IContentItem { - + // TODO + public Dictionary Elements { get; set; } } } diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs new file mode 100644 index 00000000..0afd7660 --- /dev/null +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Kontent.Ai.Delivery.Abstractions +{ + public interface IUniversalItemModelProvider + { + public Task GetContentItemGenericModelAsync(object item); + } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs b/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs index 2852986f..bea1ce05 100644 --- a/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs +++ b/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs @@ -17,6 +17,10 @@ public interface IDeliveryClient /// The instance that contains the content item with the specified codename. Task> GetItemAsync(string codename, IEnumerable parameters = null); + + // TODO + Task GetUniversalItemAsync(string codename, IEnumerable parameters = null); + /// /// Returns strongly typed content items that match the optional filtering parameters. By default, retrieves one level of linked items. /// @@ -25,6 +29,9 @@ public interface IDeliveryClient /// The instance that contains the content items. If no query parameters are specified, all content items are returned. Task> GetItemsAsync(IEnumerable parameters = null); + // TODO extension methods + Task GetUniversalItemsAsync(IEnumerable parameters = null); + /// /// Returns a feed that is used to traverse through strongly typed content items matching the optional filtering parameters. /// diff --git a/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs b/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs index 109b67ae..314a39ff 100644 --- a/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs +++ b/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs @@ -156,5 +156,17 @@ public async Task GetLanguagesAsync(IEnumerabl response => response.Languages.Any(), CacheHelpers.GetLanguagesDependencies); } + + // TODO + public Task GetUniversalItemAsync(string codename, IEnumerable parameters = null) + { + throw new NotImplementedException(); + } + + // TODO + public Task GetUniversalItemsAsync(IEnumerable parameters = null) + { + throw new NotImplementedException(); + } } } diff --git a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs index 05518834..b78f894c 100644 --- a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs +++ b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs @@ -1041,15 +1041,15 @@ public async Task CastResponse() } [Fact] - public async Task DynamicResponse() + public async Task GetUniversalItemAsync_RespondCorrectly() { _mockHttp .When($"{_baseUrl}/items/complete_content_item") .Respond("application/json", await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}complete_content_item.json"))); - var client = InitializeDeliveryClientWithCustomModelProvider(_mockHttp, new PropertyMapper());// , new CustomTypeProvider()); + var client = InitializeDeliveryClientWithCustomModelProvider(_mockHttp, new PropertyMapper()); - var response = await client.GetItemAsync("complete_content_item"); + var response = await client.GetUniversalItemAsync("complete_content_item"); Assert.All(response.Item.Elements, item => { @@ -1065,11 +1065,12 @@ public async Task DynamicResponse() switch (item.Value.Type) { case "rich_text": + // TODO Extract rich text break; case "modular_content": // TODO extract ContentElementValue> as separate type LinkedItems(SubPages)ElementValue - var linkedItems = (ContentElementValue>)item.Value; + // var linkedItems = (ContentElementValue>)item.Value; // TODO check out how to work with linked items => for tree traversal the strongly typed way // response.ApiResponse.getLinkedItem(linkedItems.Value[0]); break; @@ -1120,6 +1121,8 @@ public async Task DynamicResponse() { Assert.NotNull(item.TaxonomyGroup); }); + + Assert.Single(response.LinkedItems); } [Fact] diff --git a/Kontent.Ai.Delivery.Tests/Factories/DeliveryClientFactory.cs b/Kontent.Ai.Delivery.Tests/Factories/DeliveryClientFactory.cs index f5df9d37..ea916f22 100644 --- a/Kontent.Ai.Delivery.Tests/Factories/DeliveryClientFactory.cs +++ b/Kontent.Ai.Delivery.Tests/Factories/DeliveryClientFactory.cs @@ -16,7 +16,8 @@ internal static DeliveryClient GetMockedDeliveryClientWithProjectId( MockHttpMessageHandler httpMessageHandler = null, IModelProvider modelProvider = null, IRetryPolicyProvider resiliencePolicyProvider = null, - ITypeProvider typeProvider = null) + ITypeProvider typeProvider = null, + IUniversalItemModelProvider genericModelProvider = null) { var httpClient = GetHttpClient(httpMessageHandler); @@ -26,7 +27,9 @@ internal static DeliveryClient GetMockedDeliveryClientWithProjectId( resiliencePolicyProvider ?? A.Fake(), typeProvider ?? A.Fake(), new DeliveryHttpClient(httpClient), - Serializer + Serializer, + null, + genericModelProvider ?? A.Fake() ); return client; diff --git a/Kontent.Ai.Delivery.Tests/Models/DynamicContentItemModel.cs b/Kontent.Ai.Delivery.Tests/Models/DynamicContentItemModel.cs deleted file mode 100644 index 1600bf4e..00000000 --- a/Kontent.Ai.Delivery.Tests/Models/DynamicContentItemModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Kontent.Ai.Delivery.Abstractions; -namespace Kontent.Ai.Delivery.Tests.Models -{ - public class DynamicContentItemModel - { - public IContentItemSystemAttributes System { get; set; } - - public ContentItemElements Elements { get; set; } - } -} \ No newline at end of file diff --git a/Kontent.Ai.Delivery.Tests/ValueConverterTests.cs b/Kontent.Ai.Delivery.Tests/ValueConverterTests.cs index 94dd4ec6..5aff1fc1 100644 --- a/Kontent.Ai.Delivery.Tests/ValueConverterTests.cs +++ b/Kontent.Ai.Delivery.Tests/ValueConverterTests.cs @@ -72,7 +72,7 @@ public async void LinkedItemCodenamesValueConverter() var article = await client.GetItemAsync
("on_roasts"); - Assert.Equal(new[]{"coffee_processing_techniques", "origins_of_arabica_bourbon"}, article.Item.RelatedArticleCodenames); + Assert.Equal(new[] { "coffee_processing_techniques", "origins_of_arabica_bourbon" }, article.Item.RelatedArticleCodenames); } [Fact] @@ -120,7 +120,7 @@ public async void RichTextViaValueConverter() Assert.NotNull(hostedVideo); Assert.NotNull(tweet); } - + [Fact] public async Task AssetElementValueConverter_NoPresetSpecifiedInConfig_AssetUrlIsUntouched() { @@ -138,7 +138,7 @@ public async Task AssetElementValueConverter_NoPresetSpecifiedInConfig_AssetUrlI Assert.Equal(assetUrl, teaserImage.Url); } - + [Fact] public async Task AssetElementValueConverter_DefaultPresetSpecifiedInConfig_AssetUrlContainsDefaultRenditionQuery() { @@ -149,7 +149,7 @@ public async Task AssetElementValueConverter_DefaultPresetSpecifiedInConfig_Asse var defaultRenditionPreset = "default"; - var client = InitializeDeliveryClient(mockHttp, new DeliveryOptions { ProjectId = _guid, DefaultRenditionPreset = defaultRenditionPreset}); + var client = InitializeDeliveryClient(mockHttp, new DeliveryOptions { ProjectId = _guid, DefaultRenditionPreset = defaultRenditionPreset }); var response = await client.GetItemAsync
("coffee_beverages_explained"); var teaserImage = response.Item.TeaserImage.FirstOrDefault(); @@ -159,7 +159,7 @@ public async Task AssetElementValueConverter_DefaultPresetSpecifiedInConfig_Asse Assert.Equal($"{assetUrl}?{defaultRenditionQuery}", teaserImage.Url); } - + [Fact] public async Task AssetElementValueConverter_MobilePresetSpecifiedInConfig_AssetUrlIsUntouchedAsThereIsNoMobileRenditionSpecified() { @@ -170,8 +170,8 @@ public async Task AssetElementValueConverter_MobilePresetSpecifiedInConfig_Asset var defaultRenditionPreset = "mobile"; - var client = InitializeDeliveryClient(mockHttp, new DeliveryOptions { ProjectId = _guid, DefaultRenditionPreset = defaultRenditionPreset}); - + var client = InitializeDeliveryClient(mockHttp, new DeliveryOptions { ProjectId = _guid, DefaultRenditionPreset = defaultRenditionPreset }); + var response = await client.GetItemAsync
("coffee_beverages_explained"); var teaserImage = response.Item.TeaserImage.FirstOrDefault(); diff --git a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs index 30f725d1..d77f6d28 100644 --- a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs @@ -106,16 +106,6 @@ internal async Task GetContentItemModelAsync(Type modelType, JToken seri property.SetValue(instance, itemSystemAttributes); } } - else if (typeof(ContentItemElements).IsAssignableFrom(property.PropertyType)) - { - - var value = GetAllPropertiesValuesAsync(elementsData, property, linkedItems, context, itemSystemAttributes, processedItems, richTextPropertiesToBeProcessed); - if (value != null) - { - property.SetValue(instance, value); - } - - } else { var value = await GetPropertyValueAsync(elementsData, property, linkedItems, context, itemSystemAttributes, processedItems, richTextPropertiesToBeProcessed); @@ -221,38 +211,6 @@ async Task GetLinkedItemAsync(string codename) }; } - private ContentItemElements GetAllPropertiesValuesAsync(JObject elementsData, PropertyInfo property, JObject linkedItems, ResolvingContext context, IContentItemSystemAttributes itemSystemAttributes, Dictionary processedItems, List richTextPropertiesToBeProcessed) - { - var result = new ContentItemElements(); - foreach (var item in elementsData) - { - var key = item.Key; - var element = item.Value; - - // TODO think about value converter implementation - // TODO what about codename property - now it is not null, bit withCodename is not really nice - IContentElementValue value = (element["type"].ToString()) switch - { - // TODO do we want to use string/structured data for rich text - probably think about support both ways - "rich_text" => element.ToObject(Serializer).WithCodename(key), - "asset" => element.ToObject(Serializer).WithCodename(key), - "number" => element.ToObject(Serializer).WithCodename(key), - // TODO do we want to use string/structured data for date time => structured is OK - "date_time" => element.ToObject(Serializer).WithCodename(key), - "multiple_choice" => element.ToObject(Serializer).WithCodename(key), - "taxonomy" => element.ToObject(Serializer).WithCodename(key), - // TODO what Linked items + what SubPages? - "modular_content" => element.ToObject>>(Serializer).WithCodename(key), - "custom" => element.ToObject(Serializer).WithCodename(key), - "url_slug" => element.ToObject(Serializer).WithCodename(key), - "text" => element.ToObject(Serializer).WithCodename(key), - _ => throw new ArgumentException($"Argument type ({element["type"].ToString()}) not supported.") - }; - result.Add(key, value); - } - return result; - } - private async Task GetPropertyValueAsync(JObject elementsData, PropertyInfo property, JObject linkedItems, ResolvingContext context, IContentItemSystemAttributes itemSystemAttributes, Dictionary processedItems, List richTextPropertiesToBeProcessed) { var elementDefinition = GetElementData(elementsData, property, itemSystemAttributes); @@ -427,11 +385,6 @@ private IPropertyValueConverter GetValueConverter(PropertyInfo property) return new DateTimeContentConverter(); } - if (property.PropertyType == typeof(IEnumerable)) - { - return new AssetElementValueConverter(DeliveryOptions); - } - return null; } diff --git a/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryItemGenericResponse.cs b/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryItemGenericResponse.cs new file mode 100644 index 00000000..d97c6f89 --- /dev/null +++ b/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryItemGenericResponse.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Kontent.Ai.Delivery.Abstractions; +using Kontent.Ai.Delivery.SharedModels; + +namespace Kontent.Ai.Delivery.ContentItems.Universal +{ + internal class DeliveryUniversalItemResponse : AbstractResponse, IDeliveryUniversalItemResponse + { + public IUniversalContentItem Item { get; } + + public Dictionary LinkedItems { get; } + + public DeliveryUniversalItemResponse(IApiResponse response) : base(response) + { + ApiResponse = response; + } + + public DeliveryUniversalItemResponse(IApiResponse response, IUniversalContentItem item, Dictionary linkedItems = null) : this(response) + { + Item = item; + LinkedItems = linkedItems; + } + } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs new file mode 100644 index 00000000..7d687836 --- /dev/null +++ b/Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Kontent.Ai.Delivery.Abstractions; +using Kontent.Ai.Delivery.ContentItems.Elements; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Kontent.Ai.Delivery.ContentItems.Universal +{ + internal class GenericModelProvider : IUniversalItemModelProvider + { + internal readonly JsonSerializer Serializer; + + internal GenericModelProvider(JsonSerializer serializer) + { + Serializer = serializer; + } + + public async Task GetContentItemGenericModelAsync(object item) + => (IUniversalContentItem)await GetContentItemModelAsync((JObject)item); + + internal async Task GetContentItemModelAsync(JObject serializedItem) + { + var result = new UniversalContentItem() { + System = serializedItem?["system"]?.ToObject(Serializer) + }; + + foreach (var item in (JObject)serializedItem["elements"]) + { + var key = item.Key; + var element = item.Value; + + // TODO think about value converter implementation + // TODO what about codename property - now it is not null, bit withCodename is not really nice + IContentElementValue value = (element["type"].ToString()) switch + { + // TODO do we want to use string/structured data for rich text - probably think about support both ways + "rich_text" => element.ToObject(Serializer).WithCodename(key), + "asset" => element.ToObject(Serializer).WithCodename(key), + "number" => element.ToObject(Serializer).WithCodename(key), + // TODO do we want to use string/structured data for date time => structured is OK + "date_time" => element.ToObject(Serializer).WithCodename(key), + "multiple_choice" => element.ToObject(Serializer).WithCodename(key), + "taxonomy" => element.ToObject(Serializer).WithCodename(key), + // TODO what Linked items + what SubPages? + "modular_content" => element.ToObject>>(Serializer).WithCodename(key), + "custom" => element.ToObject(Serializer).WithCodename(key), + "url_slug" => element.ToObject(Serializer).WithCodename(key), + "text" => element.ToObject(Serializer).WithCodename(key), + _ => throw new ArgumentException($"Argument type ({element["type"].ToString()}) not supported.") + }; + result.Elements.Add(key, value); + } + + return result; + } + } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/Universal/UniversalContentItem.cs b/Kontent.Ai.Delivery/ContentItems/Universal/UniversalContentItem.cs new file mode 100644 index 00000000..f0f7ca4d --- /dev/null +++ b/Kontent.Ai.Delivery/ContentItems/Universal/UniversalContentItem.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Kontent.Ai.Delivery.Abstractions; + +namespace Kontent.Ai.Delivery.ContentItems.Universal +{ + public class UniversalContentItem : IUniversalContentItem + { + public IContentItemSystemAttributes System { get; set; } + public Dictionary Elements { get; set; } = new Dictionary(); + } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery/DeliveryClient.cs b/Kontent.Ai.Delivery/DeliveryClient.cs index f94b12b6..edb595d5 100644 --- a/Kontent.Ai.Delivery/DeliveryClient.cs +++ b/Kontent.Ai.Delivery/DeliveryClient.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Kontent.Ai.Delivery.Abstractions; using Kontent.Ai.Delivery.ContentItems; +using Kontent.Ai.Delivery.ContentItems.Universal; using Kontent.Ai.Delivery.ContentTypes; using Kontent.Ai.Delivery.Extensions; using Kontent.Ai.Delivery.Languages; @@ -37,6 +38,7 @@ internal sealed class DeliveryClient : IDeliveryClient internal readonly IDeliveryHttpClient DeliveryHttpClient; internal readonly JsonSerializer Serializer; internal readonly ILoggerFactory LoggerFactory; + internal readonly IUniversalItemModelProvider GenericModelProvider; internal DeliveryEndpointUrlBuilder UrlBuilder => _urlBuilder ??= new DeliveryEndpointUrlBuilder(DeliveryOptions); @@ -58,7 +60,9 @@ public DeliveryClient( ITypeProvider typeProvider = null, IDeliveryHttpClient deliveryHttpClient = null, JsonSerializer serializer = null, - ILoggerFactory loggerFactory = null) + // TODO why logger factory is not everywhere ? + ILoggerFactory loggerFactory = null, + IUniversalItemModelProvider genericModelProvider = null) { DeliveryOptions = deliveryOptions; ModelProvider = modelProvider; @@ -67,6 +71,8 @@ public DeliveryClient( DeliveryHttpClient = deliveryHttpClient; Serializer = serializer; LoggerFactory = loggerFactory; + // TODO IOC? Default? Check references + GenericModelProvider = new GenericModelProvider(serializer); } /// @@ -85,7 +91,7 @@ public async Task> GetItemAsync(string codename, IEn var endpointUrl = UrlBuilder.GetItemUrl(codename, parameters); var response = await GetDeliveryResponseAsync(endpointUrl); - + if (!response.IsSuccess) { return new DeliveryItemResponse(response); @@ -107,12 +113,12 @@ public async Task> GetItemsAsync(IEnumerable< var enhancedParameters = EnsureContentTypeFilter(parameters).ToList(); var endpointUrl = UrlBuilder.GetItemsUrl(enhancedParameters); var response = await GetDeliveryResponseAsync(endpointUrl); - + if (!response.IsSuccess) { return new DeliveryItemListingResponse(response); } - + var content = await response.GetJsonContentAsync(); var pagination = content["pagination"].ToObject(Serializer); var items = ((JArray)content["items"]).Select(async source => await ModelProvider.GetContentItemModelAsync(source, content["modular_content"])); @@ -136,12 +142,12 @@ public IDeliveryItemsFeed GetItemsFeed(IEnumerable parame async Task> GetItemsBatchAsync(string continuationToken) { var response = await GetDeliveryResponseAsync(endpointUrl, continuationToken); - + if (!response.IsSuccess) { return new DeliveryItemsFeedResponse(response); } - + var content = await response.GetJsonContentAsync(); var items = ((JArray)content["items"]).Select(async source => await ModelProvider.GetContentItemModelAsync(source, content["modular_content"])); @@ -194,7 +200,7 @@ public async Task GetTypesAsync(IEnumerable(Serializer); var types = content["types"].ToObject>(Serializer); @@ -280,12 +286,12 @@ public async Task GetTaxonomiesAsync(IEnumerab { var endpointUrl = UrlBuilder.GetTaxonomiesUrl(parameters); var response = await GetDeliveryResponseAsync(endpointUrl); - + if (!response.IsSuccess) { return new DeliveryTaxonomyListingResponse(response); } - + var content = await response.GetJsonContentAsync(); var pagination = content["pagination"].ToObject(Serializer); var taxonomies = content["taxonomies"].ToObject>(Serializer); @@ -301,12 +307,12 @@ public async Task GetLanguagesAsync(IEnumerabl { var endpointUrl = UrlBuilder.GetLanguagesUrl(parameters); var response = await GetDeliveryResponseAsync(endpointUrl); - + if (!response.IsSuccess) { return new DeliveryLanguageListingResponse(response); } - + var content = await response.GetJsonContentAsync(); var pagination = content["pagination"].ToObject(Serializer); var languages = content["languages"].ToObject>(Serializer); @@ -447,5 +453,43 @@ private static void ValidateItemsFeedParameters(IEnumerable par throw new ArgumentException("Skip parameter is not supported in items feed."); } } + + public async Task GetUniversalItemAsync(string codename, IEnumerable parameters = null) + { + if (string.IsNullOrEmpty(codename)) + { + throw new ArgumentException("Entered item codename is not valid.", nameof(codename)); + } + + var endpointUrl = UrlBuilder.GetItemUrl(codename, parameters); + var response = await GetDeliveryResponseAsync(endpointUrl); + + if (!response.IsSuccess) + { + return new DeliveryUniversalItemResponse(response); + } + + var content = await response.GetJsonContentAsync(); + var model = await GenericModelProvider.GetContentItemGenericModelAsync(content["item"]); + + var linkedUniversalItems = new Dictionary(); + // TODO rewrite + var result = content["modular_content"]? + .Values() + .Select(async linkedItem => + { + var res = await GenericModelProvider.GetContentItemGenericModelAsync(linkedItem); + linkedUniversalItems.Add(res.System.Codename, res); + }); + + + Task.WhenAll(result); + return new DeliveryUniversalItemResponse(response, model, linkedUniversalItems); + } + + public Task GetUniversalItemsAsync(IEnumerable parameters = null) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/docs/customization-and-extensibility/structured-models/resolving-item-links.md b/docs/customization-and-extensibility/structured-models/resolving-item-links.md index 0ffdcbd4..f9c7b57f 100644 --- a/docs/customization-and-extensibility/structured-models/resolving-item-links.md +++ b/docs/customization-and-extensibility/structured-models/resolving-item-links.md @@ -49,7 +49,7 @@ public class CustomContentLinkUrlResolver : IContentLinkUrlResolver return Task.FromResult($"/accessories/{link.UrlSlug}"); } - // TODO: Add the rest of the resolver logic + // TBD: Add the rest of the resolver logic } public Task ResolveBrokenLinkUrlAsync() From 58943b081d8c88103e28c7b81f61985b0fbfa03d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Wed, 29 Mar 2023 11:31:18 +0200 Subject: [PATCH 11/34] fix test --- Kontent.Ai.Delivery/ContentItems/ModelProvider.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs index d77f6d28..6f5fc90d 100644 --- a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs @@ -385,6 +385,11 @@ private IPropertyValueConverter GetValueConverter(PropertyInfo property) return new DateTimeContentConverter(); } + if (property.PropertyType == typeof(IEnumerable)) + { + return new AssetElementValueConverter(DeliveryOptions); + } + return null; } From 6f8d17a0ac195343df2cdc8007ad1e93c51f07c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Thu, 30 Mar 2023 11:38:44 +0200 Subject: [PATCH 12/34] add universal items async suport for listing + refactore tests --- .../DeliveryClientTests.cs | 212 +++++++++++++----- .../Elements/LinkedItemsElementValue.cs | 9 + .../DeliveryUniversalItemListingResponse.cs | 28 +++ .../Universal/GenericModelProvider.cs | 2 +- Kontent.Ai.Delivery/DeliveryClient.cs | 106 +++++---- 5 files changed, 256 insertions(+), 101 deletions(-) create mode 100644 Kontent.Ai.Delivery/ContentItems/Elements/LinkedItemsElementValue.cs create mode 100644 Kontent.Ai.Delivery/ContentItems/Universal/DeliveryUniversalItemListingResponse.cs diff --git a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs index b78f894c..64224569 100644 --- a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs +++ b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs @@ -1054,75 +1054,163 @@ public async Task GetUniversalItemAsync_RespondCorrectly() Assert.All(response.Item.Elements, item => { + Assert.NotNull(item.Value.Name); + Assert.NotNull(item.Value.Type); + Assert.NotNull(item.Value.Codename); - // Assignment with pattern matching - var x = item.Value switch - { - AssetElementValue y => y.Codename, - _ => null - }; + var element = item.Value as IContentElementValue; - switch (item.Value.Type) + switch (element) { - case "rich_text": - // TODO Extract rich text - - break; - case "modular_content": - // TODO extract ContentElementValue> as separate type LinkedItems(SubPages)ElementValue - // var linkedItems = (ContentElementValue>)item.Value; - // TODO check out how to work with linked items => for tree traversal the strongly typed way - // response.ApiResponse.getLinkedItem(linkedItems.Value[0]); - break; - default: - break; + case TextElementValue text: + { + Assert.Equal("text", text.Type); + Assert.Equal("Text field", text.Name); + Assert.Equal("text_field", text.Codename); + Assert.Equal("Text field value", text.Value); + break; + } + case RichTextElementValue richText: + { + Assert.Equal("rich_text", richText.Type); + Assert.Equal("Rich text field", richText.Name); + Assert.Equal("rich_text_field", richText.Codename); + Assert.Equal("

Rich text field value

", richText.Value); + Assert.Empty(richText.ModularContent); + Assert.Empty(richText.Images); + Assert.Empty(richText.Links); + break; + } + case NumberElementValue number: + { + Assert.Equal("number", number.Type); + Assert.Equal("Number field", number.Name); + Assert.Equal("number_field", number.Codename); + Assert.Equal(99, number.Value); + break; + } + case MultipleChoiceElementValue multipleChoice: + { + Assert.Equal("multiple_choice", multipleChoice.Type); + if (multipleChoice.Codename == "multiple_choice_field_as_radio_buttons") + { + + Assert.Equal("Multiple choice field as Radio buttons", multipleChoice.Name); + Assert.Collection(multipleChoice.Value, + item => + { + Assert.Equal("Radio button 1", item.Name); + Assert.Equal("radio_button_1", item.Codename); + }); + } + else if (multipleChoice.Codename == "multiple_choice_field_as_checkboxes") + { + Assert.Equal("Multiple choice field as Checkboxes", multipleChoice.Name); + Assert.Collection(multipleChoice.Value, + item => + { + Assert.Equal("Checkbox 1", item.Name); + Assert.Equal("checkbox_1", item.Codename); + }, + item => + { + Assert.Equal("Checkbox 2", item.Name); + Assert.Equal("checkbox_2", item.Codename); + }); + } + break; + } + case IDateTimeElementValue dateTime: + { + Assert.Equal("date_time", dateTime.Type); + Assert.Equal("Date & time field", dateTime.Name); + Assert.Equal("date___time_field", dateTime.Codename); + Assert.Equal(new DateTime(2017, 02, 23), dateTime.Value); + Assert.Equal("display_timezone", dateTime.DisplayTimezone); + break; + } + case AssetElementValue assets: + { + Assert.Equal("asset", assets.Type); + Assert.Equal("Asset field", assets.Name); + Assert.Equal("asset_field", assets.Codename); + Assert.Collection(assets.Value, + asset => + { + Assert.Equal("Fire.jpg", asset.Name); + Assert.Equal("image/jpeg", asset.Type); + Assert.Equal(129170, asset.Size); + Assert.Equal("https://assets.kontent.ai:443/e1167a11-75af-4a08-ad84-0582b463b010/64096741-b658-46ee-b148-b287fe03ea16/Fire.jpg", asset.Url); + }); + break; + } + case LinkedItemsElementValue linkedItems: + { + Assert.Equal("modular_content", linkedItems.Type); + Assert.Equal("Modular content field", linkedItems.Name); + Assert.Equal("linked_items_field", linkedItems.Codename); + Assert.Equal(new[] { "homepage" }, linkedItems.Value); + break; + } + case TaxonomyElementValue taxonomy: + { + Assert.Equal("taxonomy", taxonomy.Type); + Assert.Equal("Complete type taxonomy", taxonomy.Name); + Assert.Equal("complete_type_taxonomy", taxonomy.Codename); + Assert.Equal("complete_type_taxonomy", taxonomy.TaxonomyGroup); + + Assert.Collection(taxonomy.Value, + taxonomyTerm => + { + Assert.Equal("Option 1", taxonomyTerm.Name); + Assert.Equal("option_1", taxonomyTerm.Codename); + }, + taxonomyTerm => + { + Assert.Equal("Option 2", taxonomyTerm.Name); + Assert.Equal("option_2", taxonomyTerm.Codename); + }); + + break; + } + case UrlSlugElementValue urlSlug: + { + Assert.Equal("url_slug", urlSlug.Type); + Assert.Equal("Url slug field", urlSlug.Name); + Assert.Equal("url_slug_field", urlSlug.Codename); + Assert.Equal("complete-content-item-url-slug", urlSlug.Value); + break; + } + case CustomElementValue customElement: + { + Assert.Equal("custom", customElement.Type); + Assert.Equal("ColorPicker", customElement.Name); + Assert.Equal("custom_element_field", customElement.Codename); + Assert.Equal("#d7e119", customElement.Value); + break; + } } + }); + } - // - if (typeof(IContentElementValue).IsAssignableFrom(item.Value.GetType())) - { - var elementObject = (IContentElementValue)item.Value; - Assert.NotNull(elementObject.Value); - Assert.NotNull(elementObject.Type); - Assert.NotNull(elementObject.Name); - // TODO think about that one - // Assert.Null(elementObject.Codename); - Assert.NotNull(elementObject.Codename); - } - else if (typeof(DateTimeElementValue).IsAssignableFrom(item.Value.GetType())) - { - var elementObject = (DateTimeElementValue)item.Value; - Assert.NotNull(elementObject.Value); - Assert.NotNull(elementObject.Type); - Assert.NotNull(elementObject.Name); - // TODO think about that one - // Assert.Null(elementObject.Codename); - Assert.NotNull(elementObject.Codename); + [Fact] + public async Task GetUniversalItemsAsync_RespondCorrectly() + { + _mockHttp + .When($"{_baseUrl}/items") + .Respond("application/json", await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}items.json"))); - } - else if (typeof(NumberElementValue).IsAssignableFrom(item.Value.GetType())) - { - var elementObject = (NumberElementValue)item.Value; - Assert.NotNull(elementObject.Value); - Assert.NotNull(elementObject.Type); - Assert.NotNull(elementObject.Name); - // TODO think about that one - // Assert.Null(elementObject.Codename); - Assert.NotNull(elementObject.Codename); - } - }); - Assert.All(response.Item.Elements.OfType(), item => - { - Assert.NotNull(item.Images); - Assert.NotNull(item.Links); - Assert.NotNull(item.ModularContent); - }); - Assert.All(response.Item.Elements.OfType(), item => - { - Assert.NotNull(item.TaxonomyGroup); - }); + var client = InitializeDeliveryClientWithCustomModelProvider(_mockHttp, new PropertyMapper()); + + var response = await client.GetUniversalItemsAsync(); - Assert.Single(response.LinkedItems); + Assert.Equal(11, response.Items.Count); + Assert.Equal(5, response.LinkedItems.Count); + Assert.Equal(0,response.Pagination.Skip); + Assert.Equal(0,response.Pagination.Limit); + Assert.Equal(11,response.Pagination.Count); + Assert.Null(response.Pagination.TotalCount); + Assert.Null(response.Pagination.NextPageUrl); } [Fact] diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/LinkedItemsElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/LinkedItemsElementValue.cs new file mode 100644 index 00000000..dd552ef6 --- /dev/null +++ b/Kontent.Ai.Delivery/ContentItems/Elements/LinkedItemsElementValue.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Kontent.Ai.Delivery.ContentItems.Elements +{ + internal class LinkedItemsElementValue: ContentElementValue> + { + + } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryUniversalItemListingResponse.cs b/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryUniversalItemListingResponse.cs new file mode 100644 index 00000000..4366c5f7 --- /dev/null +++ b/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryUniversalItemListingResponse.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Kontent.Ai.Delivery.Abstractions; +using Kontent.Ai.Delivery.SharedModels; + +namespace Kontent.Ai.Delivery.ContentItems.Universal +{ + internal class DeliveryUniversalItemListingResponse : AbstractResponse, IDeliveryUniversalItemListingResponse + { + public IList Items { get; } + + public Dictionary LinkedItems { get; } + + + public IPagination Pagination { get; } + + public DeliveryUniversalItemListingResponse(IApiResponse response) : base(response) + { + ApiResponse = response; + } + + public DeliveryUniversalItemListingResponse(IApiResponse response, IList items, IPagination pagination, Dictionary linkedItems = null) : this(response) + { + Items = items; + Pagination = pagination; + LinkedItems = linkedItems; + } + } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs index 7d687836..a940fd9e 100644 --- a/Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs @@ -44,7 +44,7 @@ internal async Task GetContentItemModelAsync(JObject seri "multiple_choice" => element.ToObject(Serializer).WithCodename(key), "taxonomy" => element.ToObject(Serializer).WithCodename(key), // TODO what Linked items + what SubPages? - "modular_content" => element.ToObject>>(Serializer).WithCodename(key), + "modular_content" => element.ToObject(Serializer).WithCodename(key), "custom" => element.ToObject(Serializer).WithCodename(key), "url_slug" => element.ToObject(Serializer).WithCodename(key), "text" => element.ToObject(Serializer).WithCodename(key), diff --git a/Kontent.Ai.Delivery/DeliveryClient.cs b/Kontent.Ai.Delivery/DeliveryClient.cs index edb595d5..224829fa 100644 --- a/Kontent.Ai.Delivery/DeliveryClient.cs +++ b/Kontent.Ai.Delivery/DeliveryClient.cs @@ -319,6 +319,74 @@ public async Task GetLanguagesAsync(IEnumerabl return new DeliveryLanguageListingResponse(response, languages.ToList(), pagination); } + + public async Task GetUniversalItemAsync(string codename, IEnumerable parameters = null) + { + if (string.IsNullOrEmpty(codename)) + { + throw new ArgumentException("Entered item codename is not valid.", nameof(codename)); + } + + var endpointUrl = UrlBuilder.GetItemUrl(codename, parameters); + var response = await GetDeliveryResponseAsync(endpointUrl); + + if (!response.IsSuccess) + { + return new DeliveryUniversalItemResponse(response); + } + + var content = await response.GetJsonContentAsync(); + var model = await GenericModelProvider.GetContentItemGenericModelAsync(content["item"]); + + var linkedUniversalItems = await Task.WhenAll( + content["modular_content"]? + .Values() + .Select(async linkedItem => + { + var model = await GenericModelProvider.GetContentItemGenericModelAsync(linkedItem); + return new KeyValuePair(model.System.Codename, model); + }) + ); + + return new DeliveryUniversalItemResponse( + response, + model, + linkedUniversalItems.ToDictionary(pair => pair.Key, pair => pair.Value)); + } + + public async Task GetUniversalItemsAsync(IEnumerable parameters = null) + { + var endpointUrl = UrlBuilder.GetItemsUrl(parameters); + var response = await GetDeliveryResponseAsync(endpointUrl); + + if (!response.IsSuccess) + { + return new DeliveryUniversalItemListingResponse(response); + } + + var content = await response.GetJsonContentAsync(); + var pagination = content["pagination"].ToObject(Serializer); + + var items = ((JArray)content["items"]).Select(async source => await GenericModelProvider.GetContentItemGenericModelAsync(source)); + + var linkedUniversalItems = await Task.WhenAll( + content["modular_content"]? + .Values() + .Select(async linkedItem => + { + var model = await GenericModelProvider.GetContentItemGenericModelAsync(linkedItem); + return new KeyValuePair(model.System.Codename, model); + }) + ); + + return new DeliveryUniversalItemListingResponse( + response, + (await Task.WhenAll(items)).ToList(), + pagination, + linkedUniversalItems.ToDictionary(pair => pair.Key, pair => pair.Value) + ); + } + private async Task GetDeliveryResponseAsync(string endpointUrl, string continuationToken = null) { if (DeliveryOptions.CurrentValue.UsePreviewApi && DeliveryOptions.CurrentValue.UseSecureAccess) @@ -453,43 +521,5 @@ private static void ValidateItemsFeedParameters(IEnumerable par throw new ArgumentException("Skip parameter is not supported in items feed."); } } - - public async Task GetUniversalItemAsync(string codename, IEnumerable parameters = null) - { - if (string.IsNullOrEmpty(codename)) - { - throw new ArgumentException("Entered item codename is not valid.", nameof(codename)); - } - - var endpointUrl = UrlBuilder.GetItemUrl(codename, parameters); - var response = await GetDeliveryResponseAsync(endpointUrl); - - if (!response.IsSuccess) - { - return new DeliveryUniversalItemResponse(response); - } - - var content = await response.GetJsonContentAsync(); - var model = await GenericModelProvider.GetContentItemGenericModelAsync(content["item"]); - - var linkedUniversalItems = new Dictionary(); - // TODO rewrite - var result = content["modular_content"]? - .Values() - .Select(async linkedItem => - { - var res = await GenericModelProvider.GetContentItemGenericModelAsync(linkedItem); - linkedUniversalItems.Add(res.System.Codename, res); - }); - - - Task.WhenAll(result); - return new DeliveryUniversalItemResponse(response, model, linkedUniversalItems); - } - - public Task GetUniversalItemsAsync(IEnumerable parameters = null) - { - throw new NotImplementedException(); - } } } \ No newline at end of file From 892644f013c483eaadd22e0138b6ee996406d89a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Thu, 30 Mar 2023 16:36:41 +0200 Subject: [PATCH 13/34] unify content items property types --- .../Models/ContentTypes/Article.cs | 2 +- Kontent.Ai.Delivery.Tests/ValueConverterTests.cs | 4 ++-- .../ContentItems/Elements/StringElementValue.cs | 2 +- Kontent.Ai.Delivery/ContentItems/ModelProvider.cs | 13 +++++++++---- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Kontent.Ai.Delivery.Tests/Models/ContentTypes/Article.cs b/Kontent.Ai.Delivery.Tests/Models/ContentTypes/Article.cs index 50b9e5b3..5361c495 100644 --- a/Kontent.Ai.Delivery.Tests/Models/ContentTypes/Article.cs +++ b/Kontent.Ai.Delivery.Tests/Models/ContentTypes/Article.cs @@ -36,6 +36,6 @@ public partial class Article : IArticle [PropertyName("related_articles")] [TestLinkedItemCodenamesValueConverter] - public List RelatedArticleCodenames { get; set; } + public IEnumerable RelatedArticleCodenames { get; set; } } } \ No newline at end of file diff --git a/Kontent.Ai.Delivery.Tests/ValueConverterTests.cs b/Kontent.Ai.Delivery.Tests/ValueConverterTests.cs index 5aff1fc1..72640b6c 100644 --- a/Kontent.Ai.Delivery.Tests/ValueConverterTests.cs +++ b/Kontent.Ai.Delivery.Tests/ValueConverterTests.cs @@ -28,9 +28,9 @@ public Task GetPropertyValueAsync(PropertyInfo property, TElem } [AttributeUsage(AttributeTargets.Property)] - public class TestLinkedItemCodenamesValueConverterAttribute : Attribute, IPropertyValueConverter> + public class TestLinkedItemCodenamesValueConverterAttribute : Attribute, IPropertyValueConverter> { - public Task GetPropertyValueAsync(PropertyInfo property, TElement element, ResolvingContext context) where TElement : IContentElementValue> + public Task GetPropertyValueAsync(PropertyInfo property, TElement element, ResolvingContext context) where TElement : IContentElementValue> { return Task.FromResult((object)element.Value); } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs index f60b40f5..68f35928 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs @@ -1,6 +1,6 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - internal abstract class StringElementValue : ContentElementValue + internal class StringElementValue : ContentElementValue { } } \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs index 6f5fc90d..21544aa7 100644 --- a/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/ModelProvider.cs @@ -226,13 +226,18 @@ private async Task GetPropertyValueAsync(JObject elementsData, PropertyI { "rich_text" => await GetElementModelAsync(property, context, elementValue, valueConverter), "asset" => await GetElementModelAsync>(property, context, elementValue, valueConverter), - "number" => await GetElementModelAsync, decimal?>(property, context, elementValue, valueConverter), + "number" => await GetElementModelAsync(property, context, elementValue, valueConverter), "date_time" => await GetElementModelAsync(property, context, elementValue, valueConverter), - "multiple_choice" => await GetElementModelAsync>, List>(property, context, elementValue, valueConverter), + "multiple_choice" => await GetElementModelAsync>(property, context, elementValue, valueConverter), "taxonomy" => await GetElementModelAsync>(property, context, elementValue, valueConverter), - "modular_content" => await GetElementModelAsync>, List>(property, context, elementValue, valueConverter), + // Linked items and Subpages + "modular_content" => await GetElementModelAsync>(property, context, elementValue, valueConverter), // Custom element, text element, URL slug element - _ => await GetElementModelAsync, string>(property, context, elementValue, valueConverter), + "text" => await GetElementModelAsync(property, context, elementValue, valueConverter), + "custom" => await GetElementModelAsync(property, context, elementValue, valueConverter), + "url_slug" => await GetElementModelAsync(property, context, elementValue, valueConverter), + // Other string based elements + _ => await GetElementModelAsync(property, context, elementValue, valueConverter) }; } } From 6f11e78a4fc558990fdceb1ff6d2b4323873a5ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Thu, 30 Mar 2023 18:42:19 +0200 Subject: [PATCH 14/34] trial no.3 --- .../Universal/IUniversalModelProvider.cs | 12 -- .../IDeliveryClient.cs | 13 +- .../Kontent.Ai.Delivery.Abstractions.csproj | 1 + .../SharedModels/IApiResponse.cs | 6 +- .../DeliveryClientCache.cs | 16 +- .../DeliveryClientExtensionsTests.cs | 201 ++++++++++++++++++ ...Delivery.Extensions.Universal.Tests.csproj | 30 +++ .../Usings.cs | 1 + .../DeliveryItemGenericResponse.cs | 8 +- .../DeliveryUniversalItemListingResponse.cs | 8 +- .../Extensions/DeliveryClientExtensions.cs | 93 ++++++++ .../GenericModelProvider.cs | 42 ++-- ...nt.Ai.Delivery.Extensions.Universal.csproj | 19 ++ .../UniversalContentItem.cs | 2 +- .../DeliveryClientTests.cs | 173 --------------- .../Factories/DeliveryClientFactory.cs | 7 +- Kontent.Ai.Delivery.sln | 12 ++ .../DeliveryItemListingResponse.cs | 4 +- .../ContentItems/DeliveryItemResponse.cs | 4 +- .../Elements/AssetElementValue.cs | 2 +- .../Elements/ContentElementValue.cs | 4 +- .../Elements/CustomElementValue.cs | 2 +- .../Elements/DateTimeElementValue.cs | 2 +- .../Elements/LinkedItemsElementValue.cs | 2 +- .../Elements/MultipleChoiceElementValue.cs | 2 +- .../Elements/NumberElementValue.cs | 2 +- .../Elements/RichTextElementValue.cs | 2 +- .../Elements/StringElementValue.cs | 2 +- .../Elements/TaxonomyElementValue.cs | 2 +- .../ContentItems/Elements/TextElementValue.cs | 2 +- .../Elements/UrlSlugElementValue.cs | 2 +- .../ContentTypes/DeliveryElementResponse.cs | 4 +- .../DeliveryTypeListingResponse.cs | 4 +- .../ContentTypes/DeliveryTypeResponse.cs | 4 +- Kontent.Ai.Delivery/DeliveryClient.cs | 108 ++-------- .../DeliveryLanguageListingResponse.cs | 4 +- .../SharedModels/ApiResponse.cs | 11 +- .../SharedModels/Pagination.cs | 2 +- .../DeliveryTaxonomyListingResponse.cs | 4 +- .../DeliveryTaxonomyResponse.cs | 4 +- 40 files changed, 462 insertions(+), 361 deletions(-) delete mode 100644 Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs create mode 100644 Kontent.Ai.Delivery.Extensions.Universal.Tests/Extensions/DeliveryClientExtensionsTests.cs create mode 100644 Kontent.Ai.Delivery.Extensions.Universal.Tests/Kontent.Ai.Delivery.Extensions.Universal.Tests.csproj create mode 100644 Kontent.Ai.Delivery.Extensions.Universal.Tests/Usings.cs rename {Kontent.Ai.Delivery/ContentItems/Universal => Kontent.Ai.Delivery.Extensions.Universal}/DeliveryItemGenericResponse.cs (76%) rename {Kontent.Ai.Delivery/ContentItems/Universal => Kontent.Ai.Delivery.Extensions.Universal}/DeliveryUniversalItemListingResponse.cs (77%) create mode 100644 Kontent.Ai.Delivery.Extensions.Universal/Extensions/DeliveryClientExtensions.cs rename {Kontent.Ai.Delivery/ContentItems/Universal => Kontent.Ai.Delivery.Extensions.Universal}/GenericModelProvider.cs (65%) create mode 100644 Kontent.Ai.Delivery.Extensions.Universal/Kontent.Ai.Delivery.Extensions.Universal.csproj rename {Kontent.Ai.Delivery/ContentItems/Universal => Kontent.Ai.Delivery.Extensions.Universal}/UniversalContentItem.cs (86%) diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs deleted file mode 100644 index 0afd7660..00000000 --- a/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Kontent.Ai.Delivery.Abstractions -{ - public interface IUniversalItemModelProvider - { - public Task GetContentItemGenericModelAsync(object item); - } -} \ No newline at end of file diff --git a/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs b/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs index bea1ce05..11fb3373 100644 --- a/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs +++ b/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Extensions.Options; namespace Kontent.Ai.Delivery.Abstractions { @@ -8,6 +9,11 @@ namespace Kontent.Ai.Delivery.Abstractions /// public interface IDeliveryClient { + public IOptionsMonitor DeliveryOptions { get; } + object Serializer { get; } + + public Task GetDeliveryResponseAsync(string endpointUrl, string continuationToken = null); + /// /// Returns a strongly typed content item. By default, retrieves one level of linked items. /// @@ -17,10 +23,6 @@ public interface IDeliveryClient /// The instance that contains the content item with the specified codename. Task> GetItemAsync(string codename, IEnumerable parameters = null); - - // TODO - Task GetUniversalItemAsync(string codename, IEnumerable parameters = null); - /// /// Returns strongly typed content items that match the optional filtering parameters. By default, retrieves one level of linked items. /// @@ -29,9 +31,6 @@ public interface IDeliveryClient /// The instance that contains the content items. If no query parameters are specified, all content items are returned. Task> GetItemsAsync(IEnumerable parameters = null); - // TODO extension methods - Task GetUniversalItemsAsync(IEnumerable parameters = null); - /// /// Returns a feed that is used to traverse through strongly typed content items matching the optional filtering parameters. /// diff --git a/Kontent.Ai.Delivery.Abstractions/Kontent.Ai.Delivery.Abstractions.csproj b/Kontent.Ai.Delivery.Abstractions/Kontent.Ai.Delivery.Abstractions.csproj index b8a1023e..fe6aa516 100644 --- a/Kontent.Ai.Delivery.Abstractions/Kontent.Ai.Delivery.Abstractions.csproj +++ b/Kontent.Ai.Delivery.Abstractions/Kontent.Ai.Delivery.Abstractions.csproj @@ -27,6 +27,7 @@ + diff --git a/Kontent.Ai.Delivery.Abstractions/SharedModels/IApiResponse.cs b/Kontent.Ai.Delivery.Abstractions/SharedModels/IApiResponse.cs index 6afbc543..af713d62 100644 --- a/Kontent.Ai.Delivery.Abstractions/SharedModels/IApiResponse.cs +++ b/Kontent.Ai.Delivery.Abstractions/SharedModels/IApiResponse.cs @@ -1,4 +1,6 @@ -namespace Kontent.Ai.Delivery.Abstractions +using System.Threading.Tasks; + +namespace Kontent.Ai.Delivery.Abstractions { /// /// Represents a successful JSON response from Kontent.ai Delivery API. @@ -36,5 +38,7 @@ public interface IApiResponse /// Gets error object with message, error code. /// IError Error { get; } + + public Task GetJsonContentAsync(); } } diff --git a/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs b/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs index 314a39ff..4e86d30b 100644 --- a/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs +++ b/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using Kontent.Ai.Delivery.Abstractions; +using Microsoft.Extensions.Options; namespace Kontent.Ai.Delivery.Caching { @@ -14,6 +15,10 @@ public class DeliveryClientCache : IDeliveryClient private readonly IDeliveryClient _deliveryClient; private readonly IDeliveryCacheManager _deliveryCacheManager; + public IOptionsMonitor DeliveryOptions => _deliveryClient.DeliveryOptions; + + public object Serializer => _deliveryClient.Serializer; + /// /// Initializes a new instance of the class for retrieving cached content of the specified project. /// @@ -157,16 +162,9 @@ public async Task GetLanguagesAsync(IEnumerabl CacheHelpers.GetLanguagesDependencies); } - // TODO - public Task GetUniversalItemAsync(string codename, IEnumerable parameters = null) - { - throw new NotImplementedException(); - } - - // TODO - public Task GetUniversalItemsAsync(IEnumerable parameters = null) + public Task GetDeliveryResponseAsync(string endpointUrl, string continuationToken = null) { - throw new NotImplementedException(); + return _deliveryClient.GetDeliveryResponseAsync(endpointUrl, continuationToken); } } } diff --git a/Kontent.Ai.Delivery.Extensions.Universal.Tests/Extensions/DeliveryClientExtensionsTests.cs b/Kontent.Ai.Delivery.Extensions.Universal.Tests/Extensions/DeliveryClientExtensionsTests.cs new file mode 100644 index 00000000..1a83aa36 --- /dev/null +++ b/Kontent.Ai.Delivery.Extensions.Universal.Tests/Extensions/DeliveryClientExtensionsTests.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Kontent.Ai.Delivery.Abstractions; +using Kontent.Ai.Delivery.ContentItems.Elements; +using Kontent.Ai.Delivery.Extensions.Universal; +using RichardSzalay.MockHttp; + +namespace Kontent.Ai.Delivery.Extensions.Tests +{ + public class DeliveryClientExtensionsTests + { + private readonly Guid _guid; + private readonly string _baseUrl; + private readonly MockHttpMessageHandler _mockHttp; + + public DeliveryClientExtensionsTests() + { + _guid = Guid.NewGuid(); + var projectId = _guid.ToString(); + _baseUrl = $"https://deliver.kontent.ai/{projectId}"; + _mockHttp = new MockHttpMessageHandler(); + } + + + + + [Fact] + public async Task GetUniversalItemAsync_RespondCorrectly() + { + _mockHttp + .When($"{_baseUrl}/items/complete_content_item") + .Respond("application/json", await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}complete_content_item.json"))); + + IDeliveryClient client = null; + var response = await client.GetUniversalItemAsync("complete_content_item"); + + Assert.All(response.Item.Elements, item => + { + + Assert.NotNull(item.Value.Name); + Assert.NotNull(item.Value.Type); + Assert.NotNull(item.Value.Codename); + + var element = item.Value as IContentElementValue; + + switch (element) + { + case TextElementValue text: + { + Assert.Equal("text", text.Type); + Assert.Equal("Text field", text.Name); + Assert.Equal("text_field", text.Codename); + Assert.Equal("Text field value", text.Value); + break; + } + case RichTextElementValue richText: + { + Assert.Equal("rich_text", richText.Type); + Assert.Equal("Rich text field", richText.Name); + Assert.Equal("rich_text_field", richText.Codename); + Assert.Equal("

Rich text field value

", richText.Value); + Assert.Empty(richText.ModularContent); + Assert.Empty(richText.Images); + Assert.Empty(richText.Links); + break; + } + case NumberElementValue number: + { + Assert.Equal("number", number.Type); + Assert.Equal("Number field", number.Name); + Assert.Equal("number_field", number.Codename); + Assert.Equal(99, number.Value); + break; + } + case MultipleChoiceElementValue multipleChoice: + { + Assert.Equal("multiple_choice", multipleChoice.Type); + if (multipleChoice.Codename == "multiple_choice_field_as_radio_buttons") + { + + Assert.Equal("Multiple choice field as Radio buttons", multipleChoice.Name); + Assert.Collection(multipleChoice.Value, + item => + { + Assert.Equal("Radio button 1", item.Name); + Assert.Equal("radio_button_1", item.Codename); + }); + } + else if (multipleChoice.Codename == "multiple_choice_field_as_checkboxes") + { + Assert.Equal("Multiple choice field as Checkboxes", multipleChoice.Name); + Assert.Collection(multipleChoice.Value, + item => + { + Assert.Equal("Checkbox 1", item.Name); + Assert.Equal("checkbox_1", item.Codename); + }, + item => + { + Assert.Equal("Checkbox 2", item.Name); + Assert.Equal("checkbox_2", item.Codename); + }); + } + break; + } + case IDateTimeElementValue dateTime: + { + Assert.Equal("date_time", dateTime.Type); + Assert.Equal("Date & time field", dateTime.Name); + Assert.Equal("date___time_field", dateTime.Codename); + Assert.Equal(new DateTime(2017, 02, 23), dateTime.Value); + Assert.Equal("display_timezone", dateTime.DisplayTimezone); + break; + } + case AssetElementValue assets: + { + Assert.Equal("asset", assets.Type); + Assert.Equal("Asset field", assets.Name); + Assert.Equal("asset_field", assets.Codename); + Assert.Collection(assets.Value, + asset => + { + Assert.Equal("Fire.jpg", asset.Name); + Assert.Equal("image/jpeg", asset.Type); + Assert.Equal(129170, asset.Size); + Assert.Equal("https://assets.kontent.ai:443/e1167a11-75af-4a08-ad84-0582b463b010/64096741-b658-46ee-b148-b287fe03ea16/Fire.jpg", asset.Url); + }); + break; + } + case LinkedItemsElementValue linkedItems: + { + Assert.Equal("modular_content", linkedItems.Type); + Assert.Equal("Modular content field", linkedItems.Name); + Assert.Equal("linked_items_field", linkedItems.Codename); + Assert.Equal(new[] { "homepage" }, linkedItems.Value); + break; + } + case TaxonomyElementValue taxonomy: + { + Assert.Equal("taxonomy", taxonomy.Type); + Assert.Equal("Complete type taxonomy", taxonomy.Name); + Assert.Equal("complete_type_taxonomy", taxonomy.Codename); + Assert.Equal("complete_type_taxonomy", taxonomy.TaxonomyGroup); + + Assert.Collection(taxonomy.Value, + taxonomyTerm => + { + Assert.Equal("Option 1", taxonomyTerm.Name); + Assert.Equal("option_1", taxonomyTerm.Codename); + }, + taxonomyTerm => + { + Assert.Equal("Option 2", taxonomyTerm.Name); + Assert.Equal("option_2", taxonomyTerm.Codename); + }); + + break; + } + case UrlSlugElementValue urlSlug: + { + Assert.Equal("url_slug", urlSlug.Type); + Assert.Equal("Url slug field", urlSlug.Name); + Assert.Equal("url_slug_field", urlSlug.Codename); + Assert.Equal("complete-content-item-url-slug", urlSlug.Value); + break; + } + case CustomElementValue customElement: + { + Assert.Equal("custom", customElement.Type); + Assert.Equal("ColorPicker", customElement.Name); + Assert.Equal("custom_element_field", customElement.Codename); + Assert.Equal("#d7e119", customElement.Value); + break; + } + } + }); + } + + [Fact] + public async Task GetUniversalItemsAsync_RespondCorrectly() + { + _mockHttp + .When($"{_baseUrl}/items") + .Respond("application/json", await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}items.json"))); + + IDeliveryClient client = null; + + var response = await client.GetUniversalItemsAsync(); + + Assert.Equal(11, response.Items.Count); + Assert.Equal(5, response.LinkedItems.Count); + Assert.Equal(0, response.Pagination.Skip); + Assert.Equal(0, response.Pagination.Limit); + Assert.Equal(11, response.Pagination.Count); + Assert.Null(response.Pagination.TotalCount); + Assert.Null(response.Pagination.NextPageUrl); + } + } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery.Extensions.Universal.Tests/Kontent.Ai.Delivery.Extensions.Universal.Tests.csproj b/Kontent.Ai.Delivery.Extensions.Universal.Tests/Kontent.Ai.Delivery.Extensions.Universal.Tests.csproj new file mode 100644 index 00000000..6e715c34 --- /dev/null +++ b/Kontent.Ai.Delivery.Extensions.Universal.Tests/Kontent.Ai.Delivery.Extensions.Universal.Tests.csproj @@ -0,0 +1,30 @@ + + + + net7.0 + enable + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Kontent.Ai.Delivery.Extensions.Universal.Tests/Usings.cs b/Kontent.Ai.Delivery.Extensions.Universal.Tests/Usings.cs new file mode 100644 index 00000000..8c927eb7 --- /dev/null +++ b/Kontent.Ai.Delivery.Extensions.Universal.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryItemGenericResponse.cs b/Kontent.Ai.Delivery.Extensions.Universal/DeliveryItemGenericResponse.cs similarity index 76% rename from Kontent.Ai.Delivery/ContentItems/Universal/DeliveryItemGenericResponse.cs rename to Kontent.Ai.Delivery.Extensions.Universal/DeliveryItemGenericResponse.cs index d97c6f89..f8bb9d23 100644 --- a/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryItemGenericResponse.cs +++ b/Kontent.Ai.Delivery.Extensions.Universal/DeliveryItemGenericResponse.cs @@ -2,15 +2,17 @@ using Kontent.Ai.Delivery.Abstractions; using Kontent.Ai.Delivery.SharedModels; -namespace Kontent.Ai.Delivery.ContentItems.Universal +namespace Kontent.Ai.Delivery.Extensions.Universal { - internal class DeliveryUniversalItemResponse : AbstractResponse, IDeliveryUniversalItemResponse + internal class DeliveryUniversalItemResponse : IResponse, IDeliveryUniversalItemResponse { public IUniversalContentItem Item { get; } public Dictionary LinkedItems { get; } - public DeliveryUniversalItemResponse(IApiResponse response) : base(response) + public IApiResponse ApiResponse { get; } + + public DeliveryUniversalItemResponse(IApiResponse response) { ApiResponse = response; } diff --git a/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryUniversalItemListingResponse.cs b/Kontent.Ai.Delivery.Extensions.Universal/DeliveryUniversalItemListingResponse.cs similarity index 77% rename from Kontent.Ai.Delivery/ContentItems/Universal/DeliveryUniversalItemListingResponse.cs rename to Kontent.Ai.Delivery.Extensions.Universal/DeliveryUniversalItemListingResponse.cs index 4366c5f7..f5440425 100644 --- a/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryUniversalItemListingResponse.cs +++ b/Kontent.Ai.Delivery.Extensions.Universal/DeliveryUniversalItemListingResponse.cs @@ -2,9 +2,9 @@ using Kontent.Ai.Delivery.Abstractions; using Kontent.Ai.Delivery.SharedModels; -namespace Kontent.Ai.Delivery.ContentItems.Universal +namespace Kontent.Ai.Delivery.Extensions.Universal { - internal class DeliveryUniversalItemListingResponse : AbstractResponse, IDeliveryUniversalItemListingResponse + internal class DeliveryUniversalItemListingResponse : IResponse, IDeliveryUniversalItemListingResponse { public IList Items { get; } @@ -13,7 +13,9 @@ internal class DeliveryUniversalItemListingResponse : AbstractResponse, IDeliver public IPagination Pagination { get; } - public DeliveryUniversalItemListingResponse(IApiResponse response) : base(response) + public IApiResponse ApiResponse { get; } + + public DeliveryUniversalItemListingResponse(IApiResponse response) { ApiResponse = response; } diff --git a/Kontent.Ai.Delivery.Extensions.Universal/Extensions/DeliveryClientExtensions.cs b/Kontent.Ai.Delivery.Extensions.Universal/Extensions/DeliveryClientExtensions.cs new file mode 100644 index 00000000..2911c2dc --- /dev/null +++ b/Kontent.Ai.Delivery.Extensions.Universal/Extensions/DeliveryClientExtensions.cs @@ -0,0 +1,93 @@ +using Kontent.Ai.Delivery.Abstractions; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using AngleSharp.Html.Parser; +using Kontent.Ai.Delivery.Builders.DeliveryClient; +using Kontent.Ai.Delivery.ContentItems; +using Kontent.Ai.Delivery.ContentItems.Elements; +using Kontent.Ai.Delivery.ContentItems.RichText.Blocks; +using Kontent.Ai.Delivery.SharedModels; +using Kontent.Ai.Urls.Delivery.QueryParameters; +using Kontent.Ai.Urls.Delivery.QueryParameters.Filters; +using Newtonsoft.Json.Linq; +using Kontent.Ai.Urls.Delivery; +using Newtonsoft.Json; + +namespace Kontent.Ai.Delivery.Extensions.Universal +{ + public static class DeliveryClientExtensions + { + public static async Task GetUniversalItemAsync(this IDeliveryClient client, string codename, IEnumerable parameters = null) + { + if (string.IsNullOrEmpty(codename)) + { + throw new ArgumentException("Entered item codename is not valid.", nameof(codename)); + } + + var endpointUrl = new DeliveryEndpointUrlBuilder(client.DeliveryOptions).GetItemUrl(codename, parameters); + var response = await client.GetDeliveryResponseAsync(endpointUrl); + + if (!response.IsSuccess) + { + return new DeliveryUniversalItemResponse(response); + } + + var content = (JObject)await response.GetJsonContentAsync(); + var model = await UniversalContentItemModelProvider.GetContentItemGenericModelAsync(content["item"], (JsonSerializer)client.Serializer); + + var linkedUniversalItems = await Task.WhenAll( + content["modular_content"]? + .Values() + .Select(async linkedItem => + { + var model = await UniversalContentItemModelProvider.GetContentItemGenericModelAsync(linkedItem, (JsonSerializer)client.Serializer); + return new KeyValuePair(model.System.Codename, model); + }) + ); + + return new DeliveryUniversalItemResponse( + response, + model, + linkedUniversalItems.ToDictionary(pair => pair.Key, pair => pair.Value)); + } + + public static async Task GetUniversalItemsAsync(this IDeliveryClient client, IEnumerable parameters = null) + { + var endpointUrl = new DeliveryEndpointUrlBuilder(client.DeliveryOptions).GetItemsUrl(parameters); + var response = await client.GetDeliveryResponseAsync(endpointUrl); + + if (!response.IsSuccess) + { + return new DeliveryUniversalItemListingResponse(response); + } + + var content = (JObject)await response.GetJsonContentAsync(); + var pagination = content["pagination"].ToObject((JsonSerializer)client.Serializer); + + var items = ((JArray)content["items"]).Select(async source => await UniversalContentItemModelProvider.GetContentItemGenericModelAsync(source, (JsonSerializer)client.Serializer)); + + var linkedUniversalItems = await Task.WhenAll( + content["modular_content"]? + .Values() + .Select(async linkedItem => + { + var model = await UniversalContentItemModelProvider.GetContentItemGenericModelAsync(linkedItem, (JsonSerializer)client.Serializer); + return new KeyValuePair(model.System.Codename, model); + }) + ); + + return new DeliveryUniversalItemListingResponse( + response, + (await Task.WhenAll(items)).ToList(), + pagination, + linkedUniversalItems.ToDictionary(pair => pair.Key, pair => pair.Value) + ); + } + + } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs b/Kontent.Ai.Delivery.Extensions.Universal/GenericModelProvider.cs similarity index 65% rename from Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs rename to Kontent.Ai.Delivery.Extensions.Universal/GenericModelProvider.cs index a940fd9e..62155646 100644 --- a/Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs +++ b/Kontent.Ai.Delivery.Extensions.Universal/GenericModelProvider.cs @@ -1,29 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Kontent.Ai.Delivery.Abstractions; using Kontent.Ai.Delivery.ContentItems.Elements; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Kontent.Ai.Delivery.ContentItems.Universal +namespace Kontent.Ai.Delivery.Extensions.Universal { - internal class GenericModelProvider : IUniversalItemModelProvider + internal class UniversalContentItemModelProvider { - internal readonly JsonSerializer Serializer; + public static async Task GetContentItemGenericModelAsync(object item, JsonSerializer serializer) + => (IUniversalContentItem)await GetContentItemModelAsync((JObject)item, serializer); - internal GenericModelProvider(JsonSerializer serializer) - { - Serializer = serializer; - } - - public async Task GetContentItemGenericModelAsync(object item) - => (IUniversalContentItem)await GetContentItemModelAsync((JObject)item); - - internal async Task GetContentItemModelAsync(JObject serializedItem) + internal static async Task GetContentItemModelAsync(JObject serializedItem, JsonSerializer serializer) { var result = new UniversalContentItem() { - System = serializedItem?["system"]?.ToObject(Serializer) + System = serializedItem?["system"]?.ToObject(serializer) }; foreach (var item in (JObject)serializedItem["elements"]) @@ -36,18 +26,18 @@ internal async Task GetContentItemModelAsync(JObject seri IContentElementValue value = (element["type"].ToString()) switch { // TODO do we want to use string/structured data for rich text - probably think about support both ways - "rich_text" => element.ToObject(Serializer).WithCodename(key), - "asset" => element.ToObject(Serializer).WithCodename(key), - "number" => element.ToObject(Serializer).WithCodename(key), + "rich_text" => element.ToObject(serializer).WithCodename(key), + "asset" => element.ToObject(serializer).WithCodename(key), + "number" => element.ToObject(serializer).WithCodename(key), // TODO do we want to use string/structured data for date time => structured is OK - "date_time" => element.ToObject(Serializer).WithCodename(key), - "multiple_choice" => element.ToObject(Serializer).WithCodename(key), - "taxonomy" => element.ToObject(Serializer).WithCodename(key), + "date_time" => element.ToObject(serializer).WithCodename(key), + "multiple_choice" => element.ToObject(serializer).WithCodename(key), + "taxonomy" => element.ToObject(serializer).WithCodename(key), // TODO what Linked items + what SubPages? - "modular_content" => element.ToObject(Serializer).WithCodename(key), - "custom" => element.ToObject(Serializer).WithCodename(key), - "url_slug" => element.ToObject(Serializer).WithCodename(key), - "text" => element.ToObject(Serializer).WithCodename(key), + "modular_content" => element.ToObject(serializer).WithCodename(key), + "custom" => element.ToObject(serializer).WithCodename(key), + "url_slug" => element.ToObject(serializer).WithCodename(key), + "text" => element.ToObject(serializer).WithCodename(key), _ => throw new ArgumentException($"Argument type ({element["type"].ToString()}) not supported.") }; result.Elements.Add(key, value); diff --git a/Kontent.Ai.Delivery.Extensions.Universal/Kontent.Ai.Delivery.Extensions.Universal.csproj b/Kontent.Ai.Delivery.Extensions.Universal/Kontent.Ai.Delivery.Extensions.Universal.csproj new file mode 100644 index 00000000..d6dd3377 --- /dev/null +++ b/Kontent.Ai.Delivery.Extensions.Universal/Kontent.Ai.Delivery.Extensions.Universal.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + diff --git a/Kontent.Ai.Delivery/ContentItems/Universal/UniversalContentItem.cs b/Kontent.Ai.Delivery.Extensions.Universal/UniversalContentItem.cs similarity index 86% rename from Kontent.Ai.Delivery/ContentItems/Universal/UniversalContentItem.cs rename to Kontent.Ai.Delivery.Extensions.Universal/UniversalContentItem.cs index f0f7ca4d..e4a00e2b 100644 --- a/Kontent.Ai.Delivery/ContentItems/Universal/UniversalContentItem.cs +++ b/Kontent.Ai.Delivery.Extensions.Universal/UniversalContentItem.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Kontent.Ai.Delivery.Abstractions; -namespace Kontent.Ai.Delivery.ContentItems.Universal +namespace Kontent.Ai.Delivery.Extensions.Universal { public class UniversalContentItem : IUniversalContentItem { diff --git a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs index 64224569..baeebf8f 100644 --- a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs +++ b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs @@ -1040,179 +1040,6 @@ public async Task CastResponse() Assert.Equal("Text field value", response.Item.TextField); } - [Fact] - public async Task GetUniversalItemAsync_RespondCorrectly() - { - _mockHttp - .When($"{_baseUrl}/items/complete_content_item") - .Respond("application/json", await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}complete_content_item.json"))); - - var client = InitializeDeliveryClientWithCustomModelProvider(_mockHttp, new PropertyMapper()); - - var response = await client.GetUniversalItemAsync("complete_content_item"); - - Assert.All(response.Item.Elements, item => - { - - Assert.NotNull(item.Value.Name); - Assert.NotNull(item.Value.Type); - Assert.NotNull(item.Value.Codename); - - var element = item.Value as IContentElementValue; - - switch (element) - { - case TextElementValue text: - { - Assert.Equal("text", text.Type); - Assert.Equal("Text field", text.Name); - Assert.Equal("text_field", text.Codename); - Assert.Equal("Text field value", text.Value); - break; - } - case RichTextElementValue richText: - { - Assert.Equal("rich_text", richText.Type); - Assert.Equal("Rich text field", richText.Name); - Assert.Equal("rich_text_field", richText.Codename); - Assert.Equal("

Rich text field value

", richText.Value); - Assert.Empty(richText.ModularContent); - Assert.Empty(richText.Images); - Assert.Empty(richText.Links); - break; - } - case NumberElementValue number: - { - Assert.Equal("number", number.Type); - Assert.Equal("Number field", number.Name); - Assert.Equal("number_field", number.Codename); - Assert.Equal(99, number.Value); - break; - } - case MultipleChoiceElementValue multipleChoice: - { - Assert.Equal("multiple_choice", multipleChoice.Type); - if (multipleChoice.Codename == "multiple_choice_field_as_radio_buttons") - { - - Assert.Equal("Multiple choice field as Radio buttons", multipleChoice.Name); - Assert.Collection(multipleChoice.Value, - item => - { - Assert.Equal("Radio button 1", item.Name); - Assert.Equal("radio_button_1", item.Codename); - }); - } - else if (multipleChoice.Codename == "multiple_choice_field_as_checkboxes") - { - Assert.Equal("Multiple choice field as Checkboxes", multipleChoice.Name); - Assert.Collection(multipleChoice.Value, - item => - { - Assert.Equal("Checkbox 1", item.Name); - Assert.Equal("checkbox_1", item.Codename); - }, - item => - { - Assert.Equal("Checkbox 2", item.Name); - Assert.Equal("checkbox_2", item.Codename); - }); - } - break; - } - case IDateTimeElementValue dateTime: - { - Assert.Equal("date_time", dateTime.Type); - Assert.Equal("Date & time field", dateTime.Name); - Assert.Equal("date___time_field", dateTime.Codename); - Assert.Equal(new DateTime(2017, 02, 23), dateTime.Value); - Assert.Equal("display_timezone", dateTime.DisplayTimezone); - break; - } - case AssetElementValue assets: - { - Assert.Equal("asset", assets.Type); - Assert.Equal("Asset field", assets.Name); - Assert.Equal("asset_field", assets.Codename); - Assert.Collection(assets.Value, - asset => - { - Assert.Equal("Fire.jpg", asset.Name); - Assert.Equal("image/jpeg", asset.Type); - Assert.Equal(129170, asset.Size); - Assert.Equal("https://assets.kontent.ai:443/e1167a11-75af-4a08-ad84-0582b463b010/64096741-b658-46ee-b148-b287fe03ea16/Fire.jpg", asset.Url); - }); - break; - } - case LinkedItemsElementValue linkedItems: - { - Assert.Equal("modular_content", linkedItems.Type); - Assert.Equal("Modular content field", linkedItems.Name); - Assert.Equal("linked_items_field", linkedItems.Codename); - Assert.Equal(new[] { "homepage" }, linkedItems.Value); - break; - } - case TaxonomyElementValue taxonomy: - { - Assert.Equal("taxonomy", taxonomy.Type); - Assert.Equal("Complete type taxonomy", taxonomy.Name); - Assert.Equal("complete_type_taxonomy", taxonomy.Codename); - Assert.Equal("complete_type_taxonomy", taxonomy.TaxonomyGroup); - - Assert.Collection(taxonomy.Value, - taxonomyTerm => - { - Assert.Equal("Option 1", taxonomyTerm.Name); - Assert.Equal("option_1", taxonomyTerm.Codename); - }, - taxonomyTerm => - { - Assert.Equal("Option 2", taxonomyTerm.Name); - Assert.Equal("option_2", taxonomyTerm.Codename); - }); - - break; - } - case UrlSlugElementValue urlSlug: - { - Assert.Equal("url_slug", urlSlug.Type); - Assert.Equal("Url slug field", urlSlug.Name); - Assert.Equal("url_slug_field", urlSlug.Codename); - Assert.Equal("complete-content-item-url-slug", urlSlug.Value); - break; - } - case CustomElementValue customElement: - { - Assert.Equal("custom", customElement.Type); - Assert.Equal("ColorPicker", customElement.Name); - Assert.Equal("custom_element_field", customElement.Codename); - Assert.Equal("#d7e119", customElement.Value); - break; - } - } - }); - } - - [Fact] - public async Task GetUniversalItemsAsync_RespondCorrectly() - { - _mockHttp - .When($"{_baseUrl}/items") - .Respond("application/json", await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}items.json"))); - - var client = InitializeDeliveryClientWithCustomModelProvider(_mockHttp, new PropertyMapper()); - - var response = await client.GetUniversalItemsAsync(); - - Assert.Equal(11, response.Items.Count); - Assert.Equal(5, response.LinkedItems.Count); - Assert.Equal(0,response.Pagination.Skip); - Assert.Equal(0,response.Pagination.Limit); - Assert.Equal(11,response.Pagination.Count); - Assert.Null(response.Pagination.TotalCount); - Assert.Null(response.Pagination.NextPageUrl); - } - [Fact] public async Task CastListingResponse() { diff --git a/Kontent.Ai.Delivery.Tests/Factories/DeliveryClientFactory.cs b/Kontent.Ai.Delivery.Tests/Factories/DeliveryClientFactory.cs index ea916f22..f5df9d37 100644 --- a/Kontent.Ai.Delivery.Tests/Factories/DeliveryClientFactory.cs +++ b/Kontent.Ai.Delivery.Tests/Factories/DeliveryClientFactory.cs @@ -16,8 +16,7 @@ internal static DeliveryClient GetMockedDeliveryClientWithProjectId( MockHttpMessageHandler httpMessageHandler = null, IModelProvider modelProvider = null, IRetryPolicyProvider resiliencePolicyProvider = null, - ITypeProvider typeProvider = null, - IUniversalItemModelProvider genericModelProvider = null) + ITypeProvider typeProvider = null) { var httpClient = GetHttpClient(httpMessageHandler); @@ -27,9 +26,7 @@ internal static DeliveryClient GetMockedDeliveryClientWithProjectId( resiliencePolicyProvider ?? A.Fake(), typeProvider ?? A.Fake(), new DeliveryHttpClient(httpClient), - Serializer, - null, - genericModelProvider ?? A.Fake() + Serializer ); return client; diff --git a/Kontent.Ai.Delivery.sln b/Kontent.Ai.Delivery.sln index 8f5c7b89..2fa9cf76 100644 --- a/Kontent.Ai.Delivery.sln +++ b/Kontent.Ai.Delivery.sln @@ -34,6 +34,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kontent.Ai.Delivery.Extensi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kontent.Ai.Delivery.Abstractions.Tests", "Kontent.Ai.Delivery.Abstractions.Tests\Kontent.Ai.Delivery.Abstractions.Tests.csproj", "{F82D45A6-B0F5-4E40-A43B-4AFFF23EAF96}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kontent.Ai.Delivery.Extensions.Universal", "Kontent.Ai.Delivery.Extensions.Universal\Kontent.Ai.Delivery.Extensions.Universal.csproj", "{041F698F-4FFA-4BAB-B1AC-FA9A1DF99E12}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kontent.Ai.Delivery.Extensions.Universal.Tests", "Kontent.Ai.Delivery.Extensions.Universal.Tests\Kontent.Ai.Delivery.Extensions.Universal.Tests.csproj", "{11C5EC74-1A20-40F7-B26C-0A4E446A8851}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -92,6 +96,14 @@ Global {F82D45A6-B0F5-4E40-A43B-4AFFF23EAF96}.Debug|Any CPU.Build.0 = Debug|Any CPU {F82D45A6-B0F5-4E40-A43B-4AFFF23EAF96}.Release|Any CPU.ActiveCfg = Release|Any CPU {F82D45A6-B0F5-4E40-A43B-4AFFF23EAF96}.Release|Any CPU.Build.0 = Release|Any CPU + {041F698F-4FFA-4BAB-B1AC-FA9A1DF99E12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {041F698F-4FFA-4BAB-B1AC-FA9A1DF99E12}.Debug|Any CPU.Build.0 = Debug|Any CPU + {041F698F-4FFA-4BAB-B1AC-FA9A1DF99E12}.Release|Any CPU.ActiveCfg = Release|Any CPU + {041F698F-4FFA-4BAB-B1AC-FA9A1DF99E12}.Release|Any CPU.Build.0 = Release|Any CPU + {11C5EC74-1A20-40F7-B26C-0A4E446A8851}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11C5EC74-1A20-40F7-B26C-0A4E446A8851}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11C5EC74-1A20-40F7-B26C-0A4E446A8851}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11C5EC74-1A20-40F7-B26C-0A4E446A8851}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Kontent.Ai.Delivery/ContentItems/DeliveryItemListingResponse.cs b/Kontent.Ai.Delivery/ContentItems/DeliveryItemListingResponse.cs index f8229519..efd6976b 100644 --- a/Kontent.Ai.Delivery/ContentItems/DeliveryItemListingResponse.cs +++ b/Kontent.Ai.Delivery/ContentItems/DeliveryItemListingResponse.cs @@ -27,7 +27,7 @@ public IList Items /// A collection of content items of a specific type. /// Response paging information. [JsonConstructor] - internal DeliveryItemListingResponse(ApiResponse response, IList items,IPagination pagination) : base(response) + internal DeliveryItemListingResponse(IApiResponse response, IList items,IPagination pagination) : base(response) { Items = items; Pagination = pagination; @@ -37,7 +37,7 @@ internal DeliveryItemListingResponse(ApiResponse response, IList items,IPagin /// Initializes a new instance of the class. /// /// The response from Kontent.ai Delivery API that contains content items. - internal DeliveryItemListingResponse(ApiResponse response) : base(response) + internal DeliveryItemListingResponse(IApiResponse response) : base(response) { } } diff --git a/Kontent.Ai.Delivery/ContentItems/DeliveryItemResponse.cs b/Kontent.Ai.Delivery/ContentItems/DeliveryItemResponse.cs index 4c0662aa..74470629 100644 --- a/Kontent.Ai.Delivery/ContentItems/DeliveryItemResponse.cs +++ b/Kontent.Ai.Delivery/ContentItems/DeliveryItemResponse.cs @@ -19,7 +19,7 @@ public T Item /// The response from Kontent.ai Delivery API that contains a content item. /// Content item of a specific type. [JsonConstructor] - internal DeliveryItemResponse(ApiResponse response, T item) : base(response) + internal DeliveryItemResponse(IApiResponse response, T item) : base(response) { Item = item; } @@ -28,7 +28,7 @@ internal DeliveryItemResponse(ApiResponse response, T item) : base(response) /// Initializes a new instance of the class. /// /// The response from Kontent.ai Delivery API that contains a content item. - internal DeliveryItemResponse(ApiResponse response) : base(response) + internal DeliveryItemResponse(IApiResponse response) : base(response) { } } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/AssetElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/AssetElementValue.cs index f40a5b5d..7a24f900 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/AssetElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/AssetElementValue.cs @@ -3,7 +3,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - internal class AssetElementValue : ContentElementValue>, IAssetElementValue + public class AssetElementValue : ContentElementValue>, IAssetElementValue { } } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/ContentElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/ContentElementValue.cs index 7731114b..e3213942 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/ContentElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/ContentElementValue.cs @@ -3,7 +3,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - internal class ContentElementValue : IContentElementValue + public abstract class ContentElementValue : IContentElementValue { /// [JsonProperty("type")] @@ -21,7 +21,7 @@ internal class ContentElementValue : IContentElementValue [JsonProperty("codename")] public string Codename { get; internal set; } - // TODO is that a good idea? + // TODO is that a good idea? // extension? public ContentElementValue WithCodename(string codename) { Codename = codename; diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/CustomElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/CustomElementValue.cs index 2055240a..70d7c2fb 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/CustomElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/CustomElementValue.cs @@ -1,6 +1,6 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - internal class CustomElementValue : StringElementValue + public class CustomElementValue : StringElementValue { } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/DateTimeElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/DateTimeElementValue.cs index cdff6325..a654b7b3 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/DateTimeElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/DateTimeElementValue.cs @@ -4,7 +4,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - internal class DateTimeElementValue : ContentElementValue, IDateTimeElementValue + public class DateTimeElementValue : ContentElementValue, IDateTimeElementValue { [JsonProperty("display_timezone")] public string DisplayTimezone { get; set; } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/LinkedItemsElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/LinkedItemsElementValue.cs index dd552ef6..b7c55a87 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/LinkedItemsElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/LinkedItemsElementValue.cs @@ -2,7 +2,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - internal class LinkedItemsElementValue: ContentElementValue> + public class LinkedItemsElementValue: ContentElementValue> { } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/MultipleChoiceElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/MultipleChoiceElementValue.cs index a1cb92e3..bb5e525f 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/MultipleChoiceElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/MultipleChoiceElementValue.cs @@ -3,7 +3,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - internal class MultipleChoiceElementValue: ContentElementValue> + public class MultipleChoiceElementValue: ContentElementValue> { } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/NumberElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/NumberElementValue.cs index a55b0328..ab8db2a8 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/NumberElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/NumberElementValue.cs @@ -2,7 +2,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - internal class NumberElementValue: ContentElementValue + public class NumberElementValue: ContentElementValue { } } \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/RichTextElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/RichTextElementValue.cs index da7da5b9..28d82f0d 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/RichTextElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/RichTextElementValue.cs @@ -5,7 +5,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - internal class RichTextElementValue : StringElementValue, IRichTextElementValue + public class RichTextElementValue : StringElementValue, IRichTextElementValue { [JsonProperty("images")] public IDictionary Images { get; set; } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs index 68f35928..6773ee68 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs @@ -1,6 +1,6 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - internal class StringElementValue : ContentElementValue + public class StringElementValue : ContentElementValue { } } \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/TaxonomyElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/TaxonomyElementValue.cs index 810b7b77..6cc5f649 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/TaxonomyElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/TaxonomyElementValue.cs @@ -4,7 +4,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - internal class TaxonomyElementValue : ContentElementValue>, ITaxonomyElementValue + public class TaxonomyElementValue : ContentElementValue>, ITaxonomyElementValue { [JsonProperty("taxonomy_group")] public string TaxonomyGroup { get; set; } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/TextElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/TextElementValue.cs index efc3addb..bd379839 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/TextElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/TextElementValue.cs @@ -1,6 +1,6 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - internal class TextElementValue : StringElementValue + public class TextElementValue : StringElementValue { } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/UrlSlugElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/UrlSlugElementValue.cs index f132304c..dd647625 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/UrlSlugElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/UrlSlugElementValue.cs @@ -1,6 +1,6 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - internal class UrlSlugElementValue : StringElementValue + public class UrlSlugElementValue : StringElementValue { } diff --git a/Kontent.Ai.Delivery/ContentTypes/DeliveryElementResponse.cs b/Kontent.Ai.Delivery/ContentTypes/DeliveryElementResponse.cs index 3f9e8129..0d032d90 100644 --- a/Kontent.Ai.Delivery/ContentTypes/DeliveryElementResponse.cs +++ b/Kontent.Ai.Delivery/ContentTypes/DeliveryElementResponse.cs @@ -22,7 +22,7 @@ public IContentElement Element /// The response from Kontent.ai Delivery API that contains a content element. /// A content element. [JsonConstructor] - internal DeliveryElementResponse(ApiResponse response, IContentElement element) : base(response) + internal DeliveryElementResponse(IApiResponse response, IContentElement element) : base(response) { Element = element; } @@ -31,7 +31,7 @@ internal DeliveryElementResponse(ApiResponse response, IContentElement element) /// Initializes a new instance of the class. /// /// The response from Kontent.ai Delivery API that contains a content element. - internal DeliveryElementResponse(ApiResponse response) : base(response) + internal DeliveryElementResponse(IApiResponse response) : base(response) { } } diff --git a/Kontent.Ai.Delivery/ContentTypes/DeliveryTypeListingResponse.cs b/Kontent.Ai.Delivery/ContentTypes/DeliveryTypeListingResponse.cs index fe7ad63a..a99f63a6 100644 --- a/Kontent.Ai.Delivery/ContentTypes/DeliveryTypeListingResponse.cs +++ b/Kontent.Ai.Delivery/ContentTypes/DeliveryTypeListingResponse.cs @@ -27,7 +27,7 @@ public IList Types /// A collection of content types. /// Response paging information. [JsonConstructor] - internal DeliveryTypeListingResponse(ApiResponse response, IList types, IPagination pagination) : base(response) + internal DeliveryTypeListingResponse(IApiResponse response, IList types, IPagination pagination) : base(response) { Types = types; Pagination = pagination; @@ -37,7 +37,7 @@ internal DeliveryTypeListingResponse(ApiResponse response, IList t /// Initializes a new instance of the class. /// /// The response from Kontent.ai Delivery API that contains content types. - internal DeliveryTypeListingResponse(ApiResponse response) : base(response) + internal DeliveryTypeListingResponse(IApiResponse response) : base(response) { } } diff --git a/Kontent.Ai.Delivery/ContentTypes/DeliveryTypeResponse.cs b/Kontent.Ai.Delivery/ContentTypes/DeliveryTypeResponse.cs index 179d1396..82799bca 100644 --- a/Kontent.Ai.Delivery/ContentTypes/DeliveryTypeResponse.cs +++ b/Kontent.Ai.Delivery/ContentTypes/DeliveryTypeResponse.cs @@ -19,7 +19,7 @@ public IContentType Type /// The response from Kontent.ai Delivery API that contains a content type. /// A content type. [JsonConstructor] - internal DeliveryTypeResponse(ApiResponse response, IContentType type) : base(response) + internal DeliveryTypeResponse(IApiResponse response, IContentType type) : base(response) { Type = type; } @@ -28,7 +28,7 @@ internal DeliveryTypeResponse(ApiResponse response, IContentType type) : base(re /// Initializes a new instance of the class. /// /// The response from Kontent.ai Delivery API that contains a content type. - internal DeliveryTypeResponse(ApiResponse response) : base(response) + internal DeliveryTypeResponse(IApiResponse response) : base(response) { } } diff --git a/Kontent.Ai.Delivery/DeliveryClient.cs b/Kontent.Ai.Delivery/DeliveryClient.cs index 224829fa..92faf33a 100644 --- a/Kontent.Ai.Delivery/DeliveryClient.cs +++ b/Kontent.Ai.Delivery/DeliveryClient.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Kontent.Ai.Delivery.Abstractions; using Kontent.Ai.Delivery.ContentItems; -using Kontent.Ai.Delivery.ContentItems.Universal; using Kontent.Ai.Delivery.ContentTypes; using Kontent.Ai.Delivery.Extensions; using Kontent.Ai.Delivery.Languages; @@ -31,18 +30,21 @@ internal sealed class DeliveryClient : IDeliveryClient { private DeliveryEndpointUrlBuilder _urlBuilder; - internal readonly IOptionsMonitor DeliveryOptions; + public readonly IOptionsMonitor DeliveryOptions; internal readonly IModelProvider ModelProvider; internal readonly ITypeProvider TypeProvider; internal readonly IRetryPolicyProvider RetryPolicyProvider; internal readonly IDeliveryHttpClient DeliveryHttpClient; - internal readonly JsonSerializer Serializer; + public readonly JsonSerializer Serializer; internal readonly ILoggerFactory LoggerFactory; - internal readonly IUniversalItemModelProvider GenericModelProvider; - - internal DeliveryEndpointUrlBuilder UrlBuilder + public DeliveryEndpointUrlBuilder UrlBuilder => _urlBuilder ??= new DeliveryEndpointUrlBuilder(DeliveryOptions); + + IOptionsMonitor IDeliveryClient.DeliveryOptions => throw new NotImplementedException(); + + object IDeliveryClient.Serializer => (object)Serializer; + /// /// Initializes a new instance of the class for retrieving content of the specified project. /// @@ -61,8 +63,7 @@ public DeliveryClient( IDeliveryHttpClient deliveryHttpClient = null, JsonSerializer serializer = null, // TODO why logger factory is not everywhere ? - ILoggerFactory loggerFactory = null, - IUniversalItemModelProvider genericModelProvider = null) + ILoggerFactory loggerFactory = null) { DeliveryOptions = deliveryOptions; ModelProvider = modelProvider; @@ -71,8 +72,6 @@ public DeliveryClient( DeliveryHttpClient = deliveryHttpClient; Serializer = serializer; LoggerFactory = loggerFactory; - // TODO IOC? Default? Check references - GenericModelProvider = new GenericModelProvider(serializer); } /// @@ -97,7 +96,7 @@ public async Task> GetItemAsync(string codename, IEn return new DeliveryItemResponse(response); } - var content = await response.GetJsonContentAsync(); + var content = (JObject)(await response.GetJsonContentAsync()); var model = await ModelProvider.GetContentItemModelAsync(content?["item"], content?["modular_content"]); return new DeliveryItemResponse(response, model); } @@ -119,7 +118,7 @@ public async Task> GetItemsAsync(IEnumerable< return new DeliveryItemListingResponse(response); } - var content = await response.GetJsonContentAsync(); + var content = (JObject)await response.GetJsonContentAsync(); var pagination = content["pagination"].ToObject(Serializer); var items = ((JArray)content["items"]).Select(async source => await ModelProvider.GetContentItemModelAsync(source, content["modular_content"])); @@ -148,7 +147,7 @@ async Task> GetItemsBatchAsync(string continuationT return new DeliveryItemsFeedResponse(response); } - var content = await response.GetJsonContentAsync(); + var content = (JObject)await response.GetJsonContentAsync(); var items = ((JArray)content["items"]).Select(async source => await ModelProvider.GetContentItemModelAsync(source, content["modular_content"])); @@ -181,7 +180,7 @@ public async Task GetTypeAsync(string codename) return new DeliveryTypeResponse(response); } - var type = (await response.GetJsonContentAsync())?.ToObject(Serializer); + var type = ((JObject)await response.GetJsonContentAsync())?.ToObject(Serializer); return new DeliveryTypeResponse(response, type); } @@ -201,7 +200,8 @@ public async Task GetTypesAsync(IEnumerable(Serializer); var types = content["types"].ToObject>(Serializer); return new DeliveryTypeListingResponse(response, types.ToList(), pagination); @@ -243,7 +243,7 @@ public async Task GetContentElementAsync(string conten return new DeliveryElementResponse(response); } - var content = await response.GetJsonContentAsync(); + var content = (JObject)await response.GetJsonContentAsync(); var element = content?.ToObject(Serializer); return new DeliveryElementResponse(response, element); } @@ -273,7 +273,7 @@ public async Task GetTaxonomyAsync(string codename) return new DeliveryTaxonomyResponse(response); } - var taxonomy = (await response.GetJsonContentAsync())?.ToObject(Serializer); + var taxonomy = ((JObject)await response.GetJsonContentAsync())?.ToObject(Serializer); return new DeliveryTaxonomyResponse(response, taxonomy); } @@ -292,7 +292,7 @@ public async Task GetTaxonomiesAsync(IEnumerab return new DeliveryTaxonomyListingResponse(response); } - var content = await response.GetJsonContentAsync(); + var content = (JObject)await response.GetJsonContentAsync(); var pagination = content["pagination"].ToObject(Serializer); var taxonomies = content["taxonomies"].ToObject>(Serializer); return new DeliveryTaxonomyListingResponse(response, taxonomies.ToList(), pagination); @@ -313,81 +313,13 @@ public async Task GetLanguagesAsync(IEnumerabl return new DeliveryLanguageListingResponse(response); } - var content = await response.GetJsonContentAsync(); + var content = (JObject)await response.GetJsonContentAsync(); var pagination = content["pagination"].ToObject(Serializer); var languages = content["languages"].ToObject>(Serializer); return new DeliveryLanguageListingResponse(response, languages.ToList(), pagination); } - - public async Task GetUniversalItemAsync(string codename, IEnumerable parameters = null) - { - if (string.IsNullOrEmpty(codename)) - { - throw new ArgumentException("Entered item codename is not valid.", nameof(codename)); - } - - var endpointUrl = UrlBuilder.GetItemUrl(codename, parameters); - var response = await GetDeliveryResponseAsync(endpointUrl); - - if (!response.IsSuccess) - { - return new DeliveryUniversalItemResponse(response); - } - - var content = await response.GetJsonContentAsync(); - var model = await GenericModelProvider.GetContentItemGenericModelAsync(content["item"]); - - var linkedUniversalItems = await Task.WhenAll( - content["modular_content"]? - .Values() - .Select(async linkedItem => - { - var model = await GenericModelProvider.GetContentItemGenericModelAsync(linkedItem); - return new KeyValuePair(model.System.Codename, model); - }) - ); - - return new DeliveryUniversalItemResponse( - response, - model, - linkedUniversalItems.ToDictionary(pair => pair.Key, pair => pair.Value)); - } - - public async Task GetUniversalItemsAsync(IEnumerable parameters = null) - { - var endpointUrl = UrlBuilder.GetItemsUrl(parameters); - var response = await GetDeliveryResponseAsync(endpointUrl); - - if (!response.IsSuccess) - { - return new DeliveryUniversalItemListingResponse(response); - } - - var content = await response.GetJsonContentAsync(); - var pagination = content["pagination"].ToObject(Serializer); - - var items = ((JArray)content["items"]).Select(async source => await GenericModelProvider.GetContentItemGenericModelAsync(source)); - - var linkedUniversalItems = await Task.WhenAll( - content["modular_content"]? - .Values() - .Select(async linkedItem => - { - var model = await GenericModelProvider.GetContentItemGenericModelAsync(linkedItem); - return new KeyValuePair(model.System.Codename, model); - }) - ); - - return new DeliveryUniversalItemListingResponse( - response, - (await Task.WhenAll(items)).ToList(), - pagination, - linkedUniversalItems.ToDictionary(pair => pair.Key, pair => pair.Value) - ); - } - - private async Task GetDeliveryResponseAsync(string endpointUrl, string continuationToken = null) + public async Task GetDeliveryResponseAsync(string endpointUrl, string continuationToken = null) { if (DeliveryOptions.CurrentValue.UsePreviewApi && DeliveryOptions.CurrentValue.UseSecureAccess) { diff --git a/Kontent.Ai.Delivery/Languages/DeliveryLanguageListingResponse.cs b/Kontent.Ai.Delivery/Languages/DeliveryLanguageListingResponse.cs index 6b4d4781..e85f536a 100644 --- a/Kontent.Ai.Delivery/Languages/DeliveryLanguageListingResponse.cs +++ b/Kontent.Ai.Delivery/Languages/DeliveryLanguageListingResponse.cs @@ -27,7 +27,7 @@ public IPagination Pagination /// A collection of languages. /// Response paging information. [JsonConstructor] - internal DeliveryLanguageListingResponse(ApiResponse response, IList languages, IPagination pagination) : base(response) + internal DeliveryLanguageListingResponse(IApiResponse response, IList languages, IPagination pagination) : base(response) { Languages = languages; Pagination = pagination; @@ -37,7 +37,7 @@ internal DeliveryLanguageListingResponse(ApiResponse response, IList /// Initializes a new instance of the class. /// /// The response from Kontent.ai Delivery API that contains languages. - internal DeliveryLanguageListingResponse(ApiResponse response) : base(response) + internal DeliveryLanguageListingResponse(IApiResponse response) : base(response) { } } diff --git a/Kontent.Ai.Delivery/SharedModels/ApiResponse.cs b/Kontent.Ai.Delivery/SharedModels/ApiResponse.cs index c3a961b9..2105c672 100644 --- a/Kontent.Ai.Delivery/SharedModels/ApiResponse.cs +++ b/Kontent.Ai.Delivery/SharedModels/ApiResponse.cs @@ -13,7 +13,7 @@ namespace Kontent.Ai.Delivery.SharedModels /// Represents a successful JSON response from Kontent.ai Delivery API. /// [DebuggerDisplay("Url = {" + nameof(RequestUrl) + "}")] - internal sealed class ApiResponse : IApiResponse + public sealed class ApiResponse : IApiResponse { private JObject _jsonContent; private string _content; @@ -48,7 +48,7 @@ public string Content /// public IError Error { get; } - + /// /// Initializes a new instance of the class. /// @@ -56,7 +56,7 @@ public string Content /// Specifies whether content is stale. /// Continuation token to be used for continuing enumeration. /// URL used to retrieve this response. - internal ApiResponse(HttpContent httpContent, bool hasStaleContent, string continuationToken, string requestUrl) : + internal ApiResponse(HttpContent httpContent, bool hasStaleContent, string continuationToken, string requestUrl) : this(httpContent, hasStaleContent, continuationToken, requestUrl, null) { } @@ -101,5 +101,10 @@ public async Task GetJsonContentAsync() } return _jsonContent; } + + async Task IApiResponse.GetJsonContentAsync() + { + return await Task.FromResult((object)GetJsonContentAsync()); + } } } diff --git a/Kontent.Ai.Delivery/SharedModels/Pagination.cs b/Kontent.Ai.Delivery/SharedModels/Pagination.cs index a0196db3..23876ce2 100644 --- a/Kontent.Ai.Delivery/SharedModels/Pagination.cs +++ b/Kontent.Ai.Delivery/SharedModels/Pagination.cs @@ -6,7 +6,7 @@ namespace Kontent.Ai.Delivery.SharedModels { /// [DebuggerDisplay("Count = {" + nameof(Count) + "}, Total = {" + nameof(TotalCount) + "}")] - internal sealed class Pagination : IPagination + public sealed class Pagination : IPagination { /// public int Skip { get; } diff --git a/Kontent.Ai.Delivery/TaxonomyGroups/DeliveryTaxonomyListingResponse.cs b/Kontent.Ai.Delivery/TaxonomyGroups/DeliveryTaxonomyListingResponse.cs index 1799c324..63f83a50 100644 --- a/Kontent.Ai.Delivery/TaxonomyGroups/DeliveryTaxonomyListingResponse.cs +++ b/Kontent.Ai.Delivery/TaxonomyGroups/DeliveryTaxonomyListingResponse.cs @@ -33,7 +33,7 @@ public IList Taxonomies /// A collection of taxonomies. /// Response paging information. [JsonConstructor] - internal DeliveryTaxonomyListingResponse(ApiResponse response, IList taxonomies, IPagination pagination) : base(response) + internal DeliveryTaxonomyListingResponse(IApiResponse response, IList taxonomies, IPagination pagination) : base(response) { Taxonomies = taxonomies; Pagination = pagination; @@ -43,7 +43,7 @@ internal DeliveryTaxonomyListingResponse(ApiResponse response, IList class. /// /// The response from Kontent.ai Delivery API that contains taxonomies. - internal DeliveryTaxonomyListingResponse(ApiResponse response) : base(response) + internal DeliveryTaxonomyListingResponse(IApiResponse response) : base(response) { } } diff --git a/Kontent.Ai.Delivery/TaxonomyGroups/DeliveryTaxonomyResponse.cs b/Kontent.Ai.Delivery/TaxonomyGroups/DeliveryTaxonomyResponse.cs index b2f7a690..221a7a2b 100644 --- a/Kontent.Ai.Delivery/TaxonomyGroups/DeliveryTaxonomyResponse.cs +++ b/Kontent.Ai.Delivery/TaxonomyGroups/DeliveryTaxonomyResponse.cs @@ -20,7 +20,7 @@ public ITaxonomyGroup Taxonomy /// The response from Kontent.ai Delivery API that contains a taxonomy group. /// A taxonomy group. [JsonConstructor] - internal DeliveryTaxonomyResponse(ApiResponse response, ITaxonomyGroup taxonomy) : base(response) + internal DeliveryTaxonomyResponse(IApiResponse response, ITaxonomyGroup taxonomy) : base(response) { Taxonomy = taxonomy; } @@ -29,7 +29,7 @@ internal DeliveryTaxonomyResponse(ApiResponse response, ITaxonomyGroup taxonomy) /// Initializes a new instance of the class. /// /// The response from Kontent.ai Delivery API that contains a taxonomy group. - internal DeliveryTaxonomyResponse(ApiResponse response) : base(response) + internal DeliveryTaxonomyResponse(IApiResponse response) : base(response) { } } From bf916907543daceaaea5ba91722200a9e3b0aab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Thu, 30 Mar 2023 18:43:13 +0200 Subject: [PATCH 15/34] Revert "trial no.3" This reverts commit 6f11e78a4fc558990fdceb1ff6d2b4323873a5ef. --- .../Universal/IUniversalModelProvider.cs | 12 ++ .../IDeliveryClient.cs | 13 +- .../Kontent.Ai.Delivery.Abstractions.csproj | 1 - .../SharedModels/IApiResponse.cs | 6 +- .../DeliveryClientCache.cs | 16 +- .../DeliveryClientExtensionsTests.cs | 201 ------------------ ...Delivery.Extensions.Universal.Tests.csproj | 30 --- .../Usings.cs | 1 - .../Extensions/DeliveryClientExtensions.cs | 93 -------- ...nt.Ai.Delivery.Extensions.Universal.csproj | 19 -- .../DeliveryClientTests.cs | 173 +++++++++++++++ .../Factories/DeliveryClientFactory.cs | 7 +- Kontent.Ai.Delivery.sln | 12 -- .../DeliveryItemListingResponse.cs | 4 +- .../ContentItems/DeliveryItemResponse.cs | 4 +- .../Elements/AssetElementValue.cs | 2 +- .../Elements/ContentElementValue.cs | 4 +- .../Elements/CustomElementValue.cs | 2 +- .../Elements/DateTimeElementValue.cs | 2 +- .../Elements/LinkedItemsElementValue.cs | 2 +- .../Elements/MultipleChoiceElementValue.cs | 2 +- .../Elements/NumberElementValue.cs | 2 +- .../Elements/RichTextElementValue.cs | 2 +- .../Elements/StringElementValue.cs | 2 +- .../Elements/TaxonomyElementValue.cs | 2 +- .../ContentItems/Elements/TextElementValue.cs | 2 +- .../Elements/UrlSlugElementValue.cs | 2 +- .../Universal}/DeliveryItemGenericResponse.cs | 8 +- .../DeliveryUniversalItemListingResponse.cs | 8 +- .../Universal}/GenericModelProvider.cs | 42 ++-- .../Universal}/UniversalContentItem.cs | 2 +- .../ContentTypes/DeliveryElementResponse.cs | 4 +- .../DeliveryTypeListingResponse.cs | 4 +- .../ContentTypes/DeliveryTypeResponse.cs | 4 +- Kontent.Ai.Delivery/DeliveryClient.cs | 108 ++++++++-- .../DeliveryLanguageListingResponse.cs | 4 +- .../SharedModels/ApiResponse.cs | 11 +- .../SharedModels/Pagination.cs | 2 +- .../DeliveryTaxonomyListingResponse.cs | 4 +- .../DeliveryTaxonomyResponse.cs | 4 +- 40 files changed, 361 insertions(+), 462 deletions(-) create mode 100644 Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs delete mode 100644 Kontent.Ai.Delivery.Extensions.Universal.Tests/Extensions/DeliveryClientExtensionsTests.cs delete mode 100644 Kontent.Ai.Delivery.Extensions.Universal.Tests/Kontent.Ai.Delivery.Extensions.Universal.Tests.csproj delete mode 100644 Kontent.Ai.Delivery.Extensions.Universal.Tests/Usings.cs delete mode 100644 Kontent.Ai.Delivery.Extensions.Universal/Extensions/DeliveryClientExtensions.cs delete mode 100644 Kontent.Ai.Delivery.Extensions.Universal/Kontent.Ai.Delivery.Extensions.Universal.csproj rename {Kontent.Ai.Delivery.Extensions.Universal => Kontent.Ai.Delivery/ContentItems/Universal}/DeliveryItemGenericResponse.cs (76%) rename {Kontent.Ai.Delivery.Extensions.Universal => Kontent.Ai.Delivery/ContentItems/Universal}/DeliveryUniversalItemListingResponse.cs (77%) rename {Kontent.Ai.Delivery.Extensions.Universal => Kontent.Ai.Delivery/ContentItems/Universal}/GenericModelProvider.cs (65%) rename {Kontent.Ai.Delivery.Extensions.Universal => Kontent.Ai.Delivery/ContentItems/Universal}/UniversalContentItem.cs (86%) diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs new file mode 100644 index 00000000..0afd7660 --- /dev/null +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Kontent.Ai.Delivery.Abstractions +{ + public interface IUniversalItemModelProvider + { + public Task GetContentItemGenericModelAsync(object item); + } +} \ No newline at end of file diff --git a/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs b/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs index 11fb3373..bea1ce05 100644 --- a/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs +++ b/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.Extensions.Options; namespace Kontent.Ai.Delivery.Abstractions { @@ -9,11 +8,6 @@ namespace Kontent.Ai.Delivery.Abstractions /// public interface IDeliveryClient { - public IOptionsMonitor DeliveryOptions { get; } - object Serializer { get; } - - public Task GetDeliveryResponseAsync(string endpointUrl, string continuationToken = null); - /// /// Returns a strongly typed content item. By default, retrieves one level of linked items. /// @@ -23,6 +17,10 @@ public interface IDeliveryClient /// The instance that contains the content item with the specified codename. Task> GetItemAsync(string codename, IEnumerable parameters = null); + + // TODO + Task GetUniversalItemAsync(string codename, IEnumerable parameters = null); + /// /// Returns strongly typed content items that match the optional filtering parameters. By default, retrieves one level of linked items. /// @@ -31,6 +29,9 @@ public interface IDeliveryClient /// The instance that contains the content items. If no query parameters are specified, all content items are returned. Task> GetItemsAsync(IEnumerable parameters = null); + // TODO extension methods + Task GetUniversalItemsAsync(IEnumerable parameters = null); + /// /// Returns a feed that is used to traverse through strongly typed content items matching the optional filtering parameters. /// diff --git a/Kontent.Ai.Delivery.Abstractions/Kontent.Ai.Delivery.Abstractions.csproj b/Kontent.Ai.Delivery.Abstractions/Kontent.Ai.Delivery.Abstractions.csproj index fe6aa516..b8a1023e 100644 --- a/Kontent.Ai.Delivery.Abstractions/Kontent.Ai.Delivery.Abstractions.csproj +++ b/Kontent.Ai.Delivery.Abstractions/Kontent.Ai.Delivery.Abstractions.csproj @@ -27,7 +27,6 @@ - diff --git a/Kontent.Ai.Delivery.Abstractions/SharedModels/IApiResponse.cs b/Kontent.Ai.Delivery.Abstractions/SharedModels/IApiResponse.cs index af713d62..6afbc543 100644 --- a/Kontent.Ai.Delivery.Abstractions/SharedModels/IApiResponse.cs +++ b/Kontent.Ai.Delivery.Abstractions/SharedModels/IApiResponse.cs @@ -1,6 +1,4 @@ -using System.Threading.Tasks; - -namespace Kontent.Ai.Delivery.Abstractions +namespace Kontent.Ai.Delivery.Abstractions { /// /// Represents a successful JSON response from Kontent.ai Delivery API. @@ -38,7 +36,5 @@ public interface IApiResponse /// Gets error object with message, error code. /// IError Error { get; } - - public Task GetJsonContentAsync(); } } diff --git a/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs b/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs index 4e86d30b..314a39ff 100644 --- a/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs +++ b/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Threading.Tasks; using Kontent.Ai.Delivery.Abstractions; -using Microsoft.Extensions.Options; namespace Kontent.Ai.Delivery.Caching { @@ -15,10 +14,6 @@ public class DeliveryClientCache : IDeliveryClient private readonly IDeliveryClient _deliveryClient; private readonly IDeliveryCacheManager _deliveryCacheManager; - public IOptionsMonitor DeliveryOptions => _deliveryClient.DeliveryOptions; - - public object Serializer => _deliveryClient.Serializer; - /// /// Initializes a new instance of the class for retrieving cached content of the specified project. /// @@ -162,9 +157,16 @@ public async Task GetLanguagesAsync(IEnumerabl CacheHelpers.GetLanguagesDependencies); } - public Task GetDeliveryResponseAsync(string endpointUrl, string continuationToken = null) + // TODO + public Task GetUniversalItemAsync(string codename, IEnumerable parameters = null) + { + throw new NotImplementedException(); + } + + // TODO + public Task GetUniversalItemsAsync(IEnumerable parameters = null) { - return _deliveryClient.GetDeliveryResponseAsync(endpointUrl, continuationToken); + throw new NotImplementedException(); } } } diff --git a/Kontent.Ai.Delivery.Extensions.Universal.Tests/Extensions/DeliveryClientExtensionsTests.cs b/Kontent.Ai.Delivery.Extensions.Universal.Tests/Extensions/DeliveryClientExtensionsTests.cs deleted file mode 100644 index 1a83aa36..00000000 --- a/Kontent.Ai.Delivery.Extensions.Universal.Tests/Extensions/DeliveryClientExtensionsTests.cs +++ /dev/null @@ -1,201 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Kontent.Ai.Delivery.Abstractions; -using Kontent.Ai.Delivery.ContentItems.Elements; -using Kontent.Ai.Delivery.Extensions.Universal; -using RichardSzalay.MockHttp; - -namespace Kontent.Ai.Delivery.Extensions.Tests -{ - public class DeliveryClientExtensionsTests - { - private readonly Guid _guid; - private readonly string _baseUrl; - private readonly MockHttpMessageHandler _mockHttp; - - public DeliveryClientExtensionsTests() - { - _guid = Guid.NewGuid(); - var projectId = _guid.ToString(); - _baseUrl = $"https://deliver.kontent.ai/{projectId}"; - _mockHttp = new MockHttpMessageHandler(); - } - - - - - [Fact] - public async Task GetUniversalItemAsync_RespondCorrectly() - { - _mockHttp - .When($"{_baseUrl}/items/complete_content_item") - .Respond("application/json", await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}complete_content_item.json"))); - - IDeliveryClient client = null; - var response = await client.GetUniversalItemAsync("complete_content_item"); - - Assert.All(response.Item.Elements, item => - { - - Assert.NotNull(item.Value.Name); - Assert.NotNull(item.Value.Type); - Assert.NotNull(item.Value.Codename); - - var element = item.Value as IContentElementValue; - - switch (element) - { - case TextElementValue text: - { - Assert.Equal("text", text.Type); - Assert.Equal("Text field", text.Name); - Assert.Equal("text_field", text.Codename); - Assert.Equal("Text field value", text.Value); - break; - } - case RichTextElementValue richText: - { - Assert.Equal("rich_text", richText.Type); - Assert.Equal("Rich text field", richText.Name); - Assert.Equal("rich_text_field", richText.Codename); - Assert.Equal("

Rich text field value

", richText.Value); - Assert.Empty(richText.ModularContent); - Assert.Empty(richText.Images); - Assert.Empty(richText.Links); - break; - } - case NumberElementValue number: - { - Assert.Equal("number", number.Type); - Assert.Equal("Number field", number.Name); - Assert.Equal("number_field", number.Codename); - Assert.Equal(99, number.Value); - break; - } - case MultipleChoiceElementValue multipleChoice: - { - Assert.Equal("multiple_choice", multipleChoice.Type); - if (multipleChoice.Codename == "multiple_choice_field_as_radio_buttons") - { - - Assert.Equal("Multiple choice field as Radio buttons", multipleChoice.Name); - Assert.Collection(multipleChoice.Value, - item => - { - Assert.Equal("Radio button 1", item.Name); - Assert.Equal("radio_button_1", item.Codename); - }); - } - else if (multipleChoice.Codename == "multiple_choice_field_as_checkboxes") - { - Assert.Equal("Multiple choice field as Checkboxes", multipleChoice.Name); - Assert.Collection(multipleChoice.Value, - item => - { - Assert.Equal("Checkbox 1", item.Name); - Assert.Equal("checkbox_1", item.Codename); - }, - item => - { - Assert.Equal("Checkbox 2", item.Name); - Assert.Equal("checkbox_2", item.Codename); - }); - } - break; - } - case IDateTimeElementValue dateTime: - { - Assert.Equal("date_time", dateTime.Type); - Assert.Equal("Date & time field", dateTime.Name); - Assert.Equal("date___time_field", dateTime.Codename); - Assert.Equal(new DateTime(2017, 02, 23), dateTime.Value); - Assert.Equal("display_timezone", dateTime.DisplayTimezone); - break; - } - case AssetElementValue assets: - { - Assert.Equal("asset", assets.Type); - Assert.Equal("Asset field", assets.Name); - Assert.Equal("asset_field", assets.Codename); - Assert.Collection(assets.Value, - asset => - { - Assert.Equal("Fire.jpg", asset.Name); - Assert.Equal("image/jpeg", asset.Type); - Assert.Equal(129170, asset.Size); - Assert.Equal("https://assets.kontent.ai:443/e1167a11-75af-4a08-ad84-0582b463b010/64096741-b658-46ee-b148-b287fe03ea16/Fire.jpg", asset.Url); - }); - break; - } - case LinkedItemsElementValue linkedItems: - { - Assert.Equal("modular_content", linkedItems.Type); - Assert.Equal("Modular content field", linkedItems.Name); - Assert.Equal("linked_items_field", linkedItems.Codename); - Assert.Equal(new[] { "homepage" }, linkedItems.Value); - break; - } - case TaxonomyElementValue taxonomy: - { - Assert.Equal("taxonomy", taxonomy.Type); - Assert.Equal("Complete type taxonomy", taxonomy.Name); - Assert.Equal("complete_type_taxonomy", taxonomy.Codename); - Assert.Equal("complete_type_taxonomy", taxonomy.TaxonomyGroup); - - Assert.Collection(taxonomy.Value, - taxonomyTerm => - { - Assert.Equal("Option 1", taxonomyTerm.Name); - Assert.Equal("option_1", taxonomyTerm.Codename); - }, - taxonomyTerm => - { - Assert.Equal("Option 2", taxonomyTerm.Name); - Assert.Equal("option_2", taxonomyTerm.Codename); - }); - - break; - } - case UrlSlugElementValue urlSlug: - { - Assert.Equal("url_slug", urlSlug.Type); - Assert.Equal("Url slug field", urlSlug.Name); - Assert.Equal("url_slug_field", urlSlug.Codename); - Assert.Equal("complete-content-item-url-slug", urlSlug.Value); - break; - } - case CustomElementValue customElement: - { - Assert.Equal("custom", customElement.Type); - Assert.Equal("ColorPicker", customElement.Name); - Assert.Equal("custom_element_field", customElement.Codename); - Assert.Equal("#d7e119", customElement.Value); - break; - } - } - }); - } - - [Fact] - public async Task GetUniversalItemsAsync_RespondCorrectly() - { - _mockHttp - .When($"{_baseUrl}/items") - .Respond("application/json", await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}items.json"))); - - IDeliveryClient client = null; - - var response = await client.GetUniversalItemsAsync(); - - Assert.Equal(11, response.Items.Count); - Assert.Equal(5, response.LinkedItems.Count); - Assert.Equal(0, response.Pagination.Skip); - Assert.Equal(0, response.Pagination.Limit); - Assert.Equal(11, response.Pagination.Count); - Assert.Null(response.Pagination.TotalCount); - Assert.Null(response.Pagination.NextPageUrl); - } - } -} \ No newline at end of file diff --git a/Kontent.Ai.Delivery.Extensions.Universal.Tests/Kontent.Ai.Delivery.Extensions.Universal.Tests.csproj b/Kontent.Ai.Delivery.Extensions.Universal.Tests/Kontent.Ai.Delivery.Extensions.Universal.Tests.csproj deleted file mode 100644 index 6e715c34..00000000 --- a/Kontent.Ai.Delivery.Extensions.Universal.Tests/Kontent.Ai.Delivery.Extensions.Universal.Tests.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - net7.0 - enable - enable - - false - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/Kontent.Ai.Delivery.Extensions.Universal.Tests/Usings.cs b/Kontent.Ai.Delivery.Extensions.Universal.Tests/Usings.cs deleted file mode 100644 index 8c927eb7..00000000 --- a/Kontent.Ai.Delivery.Extensions.Universal.Tests/Usings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Xunit; \ No newline at end of file diff --git a/Kontent.Ai.Delivery.Extensions.Universal/Extensions/DeliveryClientExtensions.cs b/Kontent.Ai.Delivery.Extensions.Universal/Extensions/DeliveryClientExtensions.cs deleted file mode 100644 index 2911c2dc..00000000 --- a/Kontent.Ai.Delivery.Extensions.Universal/Extensions/DeliveryClientExtensions.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Kontent.Ai.Delivery.Abstractions; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using AngleSharp.Html.Parser; -using Kontent.Ai.Delivery.Builders.DeliveryClient; -using Kontent.Ai.Delivery.ContentItems; -using Kontent.Ai.Delivery.ContentItems.Elements; -using Kontent.Ai.Delivery.ContentItems.RichText.Blocks; -using Kontent.Ai.Delivery.SharedModels; -using Kontent.Ai.Urls.Delivery.QueryParameters; -using Kontent.Ai.Urls.Delivery.QueryParameters.Filters; -using Newtonsoft.Json.Linq; -using Kontent.Ai.Urls.Delivery; -using Newtonsoft.Json; - -namespace Kontent.Ai.Delivery.Extensions.Universal -{ - public static class DeliveryClientExtensions - { - public static async Task GetUniversalItemAsync(this IDeliveryClient client, string codename, IEnumerable parameters = null) - { - if (string.IsNullOrEmpty(codename)) - { - throw new ArgumentException("Entered item codename is not valid.", nameof(codename)); - } - - var endpointUrl = new DeliveryEndpointUrlBuilder(client.DeliveryOptions).GetItemUrl(codename, parameters); - var response = await client.GetDeliveryResponseAsync(endpointUrl); - - if (!response.IsSuccess) - { - return new DeliveryUniversalItemResponse(response); - } - - var content = (JObject)await response.GetJsonContentAsync(); - var model = await UniversalContentItemModelProvider.GetContentItemGenericModelAsync(content["item"], (JsonSerializer)client.Serializer); - - var linkedUniversalItems = await Task.WhenAll( - content["modular_content"]? - .Values() - .Select(async linkedItem => - { - var model = await UniversalContentItemModelProvider.GetContentItemGenericModelAsync(linkedItem, (JsonSerializer)client.Serializer); - return new KeyValuePair(model.System.Codename, model); - }) - ); - - return new DeliveryUniversalItemResponse( - response, - model, - linkedUniversalItems.ToDictionary(pair => pair.Key, pair => pair.Value)); - } - - public static async Task GetUniversalItemsAsync(this IDeliveryClient client, IEnumerable parameters = null) - { - var endpointUrl = new DeliveryEndpointUrlBuilder(client.DeliveryOptions).GetItemsUrl(parameters); - var response = await client.GetDeliveryResponseAsync(endpointUrl); - - if (!response.IsSuccess) - { - return new DeliveryUniversalItemListingResponse(response); - } - - var content = (JObject)await response.GetJsonContentAsync(); - var pagination = content["pagination"].ToObject((JsonSerializer)client.Serializer); - - var items = ((JArray)content["items"]).Select(async source => await UniversalContentItemModelProvider.GetContentItemGenericModelAsync(source, (JsonSerializer)client.Serializer)); - - var linkedUniversalItems = await Task.WhenAll( - content["modular_content"]? - .Values() - .Select(async linkedItem => - { - var model = await UniversalContentItemModelProvider.GetContentItemGenericModelAsync(linkedItem, (JsonSerializer)client.Serializer); - return new KeyValuePair(model.System.Codename, model); - }) - ); - - return new DeliveryUniversalItemListingResponse( - response, - (await Task.WhenAll(items)).ToList(), - pagination, - linkedUniversalItems.ToDictionary(pair => pair.Key, pair => pair.Value) - ); - } - - } -} \ No newline at end of file diff --git a/Kontent.Ai.Delivery.Extensions.Universal/Kontent.Ai.Delivery.Extensions.Universal.csproj b/Kontent.Ai.Delivery.Extensions.Universal/Kontent.Ai.Delivery.Extensions.Universal.csproj deleted file mode 100644 index d6dd3377..00000000 --- a/Kontent.Ai.Delivery.Extensions.Universal/Kontent.Ai.Delivery.Extensions.Universal.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - net6.0 - enable - enable - - - - - - - - - - - - - diff --git a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs index baeebf8f..64224569 100644 --- a/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs +++ b/Kontent.Ai.Delivery.Tests/DeliveryClientTests.cs @@ -1040,6 +1040,179 @@ public async Task CastResponse() Assert.Equal("Text field value", response.Item.TextField); } + [Fact] + public async Task GetUniversalItemAsync_RespondCorrectly() + { + _mockHttp + .When($"{_baseUrl}/items/complete_content_item") + .Respond("application/json", await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}complete_content_item.json"))); + + var client = InitializeDeliveryClientWithCustomModelProvider(_mockHttp, new PropertyMapper()); + + var response = await client.GetUniversalItemAsync("complete_content_item"); + + Assert.All(response.Item.Elements, item => + { + + Assert.NotNull(item.Value.Name); + Assert.NotNull(item.Value.Type); + Assert.NotNull(item.Value.Codename); + + var element = item.Value as IContentElementValue; + + switch (element) + { + case TextElementValue text: + { + Assert.Equal("text", text.Type); + Assert.Equal("Text field", text.Name); + Assert.Equal("text_field", text.Codename); + Assert.Equal("Text field value", text.Value); + break; + } + case RichTextElementValue richText: + { + Assert.Equal("rich_text", richText.Type); + Assert.Equal("Rich text field", richText.Name); + Assert.Equal("rich_text_field", richText.Codename); + Assert.Equal("

Rich text field value

", richText.Value); + Assert.Empty(richText.ModularContent); + Assert.Empty(richText.Images); + Assert.Empty(richText.Links); + break; + } + case NumberElementValue number: + { + Assert.Equal("number", number.Type); + Assert.Equal("Number field", number.Name); + Assert.Equal("number_field", number.Codename); + Assert.Equal(99, number.Value); + break; + } + case MultipleChoiceElementValue multipleChoice: + { + Assert.Equal("multiple_choice", multipleChoice.Type); + if (multipleChoice.Codename == "multiple_choice_field_as_radio_buttons") + { + + Assert.Equal("Multiple choice field as Radio buttons", multipleChoice.Name); + Assert.Collection(multipleChoice.Value, + item => + { + Assert.Equal("Radio button 1", item.Name); + Assert.Equal("radio_button_1", item.Codename); + }); + } + else if (multipleChoice.Codename == "multiple_choice_field_as_checkboxes") + { + Assert.Equal("Multiple choice field as Checkboxes", multipleChoice.Name); + Assert.Collection(multipleChoice.Value, + item => + { + Assert.Equal("Checkbox 1", item.Name); + Assert.Equal("checkbox_1", item.Codename); + }, + item => + { + Assert.Equal("Checkbox 2", item.Name); + Assert.Equal("checkbox_2", item.Codename); + }); + } + break; + } + case IDateTimeElementValue dateTime: + { + Assert.Equal("date_time", dateTime.Type); + Assert.Equal("Date & time field", dateTime.Name); + Assert.Equal("date___time_field", dateTime.Codename); + Assert.Equal(new DateTime(2017, 02, 23), dateTime.Value); + Assert.Equal("display_timezone", dateTime.DisplayTimezone); + break; + } + case AssetElementValue assets: + { + Assert.Equal("asset", assets.Type); + Assert.Equal("Asset field", assets.Name); + Assert.Equal("asset_field", assets.Codename); + Assert.Collection(assets.Value, + asset => + { + Assert.Equal("Fire.jpg", asset.Name); + Assert.Equal("image/jpeg", asset.Type); + Assert.Equal(129170, asset.Size); + Assert.Equal("https://assets.kontent.ai:443/e1167a11-75af-4a08-ad84-0582b463b010/64096741-b658-46ee-b148-b287fe03ea16/Fire.jpg", asset.Url); + }); + break; + } + case LinkedItemsElementValue linkedItems: + { + Assert.Equal("modular_content", linkedItems.Type); + Assert.Equal("Modular content field", linkedItems.Name); + Assert.Equal("linked_items_field", linkedItems.Codename); + Assert.Equal(new[] { "homepage" }, linkedItems.Value); + break; + } + case TaxonomyElementValue taxonomy: + { + Assert.Equal("taxonomy", taxonomy.Type); + Assert.Equal("Complete type taxonomy", taxonomy.Name); + Assert.Equal("complete_type_taxonomy", taxonomy.Codename); + Assert.Equal("complete_type_taxonomy", taxonomy.TaxonomyGroup); + + Assert.Collection(taxonomy.Value, + taxonomyTerm => + { + Assert.Equal("Option 1", taxonomyTerm.Name); + Assert.Equal("option_1", taxonomyTerm.Codename); + }, + taxonomyTerm => + { + Assert.Equal("Option 2", taxonomyTerm.Name); + Assert.Equal("option_2", taxonomyTerm.Codename); + }); + + break; + } + case UrlSlugElementValue urlSlug: + { + Assert.Equal("url_slug", urlSlug.Type); + Assert.Equal("Url slug field", urlSlug.Name); + Assert.Equal("url_slug_field", urlSlug.Codename); + Assert.Equal("complete-content-item-url-slug", urlSlug.Value); + break; + } + case CustomElementValue customElement: + { + Assert.Equal("custom", customElement.Type); + Assert.Equal("ColorPicker", customElement.Name); + Assert.Equal("custom_element_field", customElement.Codename); + Assert.Equal("#d7e119", customElement.Value); + break; + } + } + }); + } + + [Fact] + public async Task GetUniversalItemsAsync_RespondCorrectly() + { + _mockHttp + .When($"{_baseUrl}/items") + .Respond("application/json", await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}items.json"))); + + var client = InitializeDeliveryClientWithCustomModelProvider(_mockHttp, new PropertyMapper()); + + var response = await client.GetUniversalItemsAsync(); + + Assert.Equal(11, response.Items.Count); + Assert.Equal(5, response.LinkedItems.Count); + Assert.Equal(0,response.Pagination.Skip); + Assert.Equal(0,response.Pagination.Limit); + Assert.Equal(11,response.Pagination.Count); + Assert.Null(response.Pagination.TotalCount); + Assert.Null(response.Pagination.NextPageUrl); + } + [Fact] public async Task CastListingResponse() { diff --git a/Kontent.Ai.Delivery.Tests/Factories/DeliveryClientFactory.cs b/Kontent.Ai.Delivery.Tests/Factories/DeliveryClientFactory.cs index f5df9d37..ea916f22 100644 --- a/Kontent.Ai.Delivery.Tests/Factories/DeliveryClientFactory.cs +++ b/Kontent.Ai.Delivery.Tests/Factories/DeliveryClientFactory.cs @@ -16,7 +16,8 @@ internal static DeliveryClient GetMockedDeliveryClientWithProjectId( MockHttpMessageHandler httpMessageHandler = null, IModelProvider modelProvider = null, IRetryPolicyProvider resiliencePolicyProvider = null, - ITypeProvider typeProvider = null) + ITypeProvider typeProvider = null, + IUniversalItemModelProvider genericModelProvider = null) { var httpClient = GetHttpClient(httpMessageHandler); @@ -26,7 +27,9 @@ internal static DeliveryClient GetMockedDeliveryClientWithProjectId( resiliencePolicyProvider ?? A.Fake(), typeProvider ?? A.Fake(), new DeliveryHttpClient(httpClient), - Serializer + Serializer, + null, + genericModelProvider ?? A.Fake() ); return client; diff --git a/Kontent.Ai.Delivery.sln b/Kontent.Ai.Delivery.sln index 2fa9cf76..8f5c7b89 100644 --- a/Kontent.Ai.Delivery.sln +++ b/Kontent.Ai.Delivery.sln @@ -34,10 +34,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kontent.Ai.Delivery.Extensi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kontent.Ai.Delivery.Abstractions.Tests", "Kontent.Ai.Delivery.Abstractions.Tests\Kontent.Ai.Delivery.Abstractions.Tests.csproj", "{F82D45A6-B0F5-4E40-A43B-4AFFF23EAF96}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kontent.Ai.Delivery.Extensions.Universal", "Kontent.Ai.Delivery.Extensions.Universal\Kontent.Ai.Delivery.Extensions.Universal.csproj", "{041F698F-4FFA-4BAB-B1AC-FA9A1DF99E12}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kontent.Ai.Delivery.Extensions.Universal.Tests", "Kontent.Ai.Delivery.Extensions.Universal.Tests\Kontent.Ai.Delivery.Extensions.Universal.Tests.csproj", "{11C5EC74-1A20-40F7-B26C-0A4E446A8851}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -96,14 +92,6 @@ Global {F82D45A6-B0F5-4E40-A43B-4AFFF23EAF96}.Debug|Any CPU.Build.0 = Debug|Any CPU {F82D45A6-B0F5-4E40-A43B-4AFFF23EAF96}.Release|Any CPU.ActiveCfg = Release|Any CPU {F82D45A6-B0F5-4E40-A43B-4AFFF23EAF96}.Release|Any CPU.Build.0 = Release|Any CPU - {041F698F-4FFA-4BAB-B1AC-FA9A1DF99E12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {041F698F-4FFA-4BAB-B1AC-FA9A1DF99E12}.Debug|Any CPU.Build.0 = Debug|Any CPU - {041F698F-4FFA-4BAB-B1AC-FA9A1DF99E12}.Release|Any CPU.ActiveCfg = Release|Any CPU - {041F698F-4FFA-4BAB-B1AC-FA9A1DF99E12}.Release|Any CPU.Build.0 = Release|Any CPU - {11C5EC74-1A20-40F7-B26C-0A4E446A8851}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {11C5EC74-1A20-40F7-B26C-0A4E446A8851}.Debug|Any CPU.Build.0 = Debug|Any CPU - {11C5EC74-1A20-40F7-B26C-0A4E446A8851}.Release|Any CPU.ActiveCfg = Release|Any CPU - {11C5EC74-1A20-40F7-B26C-0A4E446A8851}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Kontent.Ai.Delivery/ContentItems/DeliveryItemListingResponse.cs b/Kontent.Ai.Delivery/ContentItems/DeliveryItemListingResponse.cs index efd6976b..f8229519 100644 --- a/Kontent.Ai.Delivery/ContentItems/DeliveryItemListingResponse.cs +++ b/Kontent.Ai.Delivery/ContentItems/DeliveryItemListingResponse.cs @@ -27,7 +27,7 @@ public IList Items /// A collection of content items of a specific type. /// Response paging information. [JsonConstructor] - internal DeliveryItemListingResponse(IApiResponse response, IList items,IPagination pagination) : base(response) + internal DeliveryItemListingResponse(ApiResponse response, IList items,IPagination pagination) : base(response) { Items = items; Pagination = pagination; @@ -37,7 +37,7 @@ internal DeliveryItemListingResponse(IApiResponse response, IList items,IPagi /// Initializes a new instance of the class. /// /// The response from Kontent.ai Delivery API that contains content items. - internal DeliveryItemListingResponse(IApiResponse response) : base(response) + internal DeliveryItemListingResponse(ApiResponse response) : base(response) { } } diff --git a/Kontent.Ai.Delivery/ContentItems/DeliveryItemResponse.cs b/Kontent.Ai.Delivery/ContentItems/DeliveryItemResponse.cs index 74470629..4c0662aa 100644 --- a/Kontent.Ai.Delivery/ContentItems/DeliveryItemResponse.cs +++ b/Kontent.Ai.Delivery/ContentItems/DeliveryItemResponse.cs @@ -19,7 +19,7 @@ public T Item /// The response from Kontent.ai Delivery API that contains a content item. /// Content item of a specific type. [JsonConstructor] - internal DeliveryItemResponse(IApiResponse response, T item) : base(response) + internal DeliveryItemResponse(ApiResponse response, T item) : base(response) { Item = item; } @@ -28,7 +28,7 @@ internal DeliveryItemResponse(IApiResponse response, T item) : base(response) /// Initializes a new instance of the class. /// /// The response from Kontent.ai Delivery API that contains a content item. - internal DeliveryItemResponse(IApiResponse response) : base(response) + internal DeliveryItemResponse(ApiResponse response) : base(response) { } } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/AssetElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/AssetElementValue.cs index 7a24f900..f40a5b5d 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/AssetElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/AssetElementValue.cs @@ -3,7 +3,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - public class AssetElementValue : ContentElementValue>, IAssetElementValue + internal class AssetElementValue : ContentElementValue>, IAssetElementValue { } } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/ContentElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/ContentElementValue.cs index e3213942..7731114b 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/ContentElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/ContentElementValue.cs @@ -3,7 +3,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - public abstract class ContentElementValue : IContentElementValue + internal class ContentElementValue : IContentElementValue { /// [JsonProperty("type")] @@ -21,7 +21,7 @@ public abstract class ContentElementValue : IContentElementValue [JsonProperty("codename")] public string Codename { get; internal set; } - // TODO is that a good idea? // extension? + // TODO is that a good idea? public ContentElementValue WithCodename(string codename) { Codename = codename; diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/CustomElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/CustomElementValue.cs index 70d7c2fb..2055240a 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/CustomElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/CustomElementValue.cs @@ -1,6 +1,6 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - public class CustomElementValue : StringElementValue + internal class CustomElementValue : StringElementValue { } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/DateTimeElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/DateTimeElementValue.cs index a654b7b3..cdff6325 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/DateTimeElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/DateTimeElementValue.cs @@ -4,7 +4,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - public class DateTimeElementValue : ContentElementValue, IDateTimeElementValue + internal class DateTimeElementValue : ContentElementValue, IDateTimeElementValue { [JsonProperty("display_timezone")] public string DisplayTimezone { get; set; } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/LinkedItemsElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/LinkedItemsElementValue.cs index b7c55a87..dd552ef6 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/LinkedItemsElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/LinkedItemsElementValue.cs @@ -2,7 +2,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - public class LinkedItemsElementValue: ContentElementValue> + internal class LinkedItemsElementValue: ContentElementValue> { } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/MultipleChoiceElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/MultipleChoiceElementValue.cs index bb5e525f..a1cb92e3 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/MultipleChoiceElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/MultipleChoiceElementValue.cs @@ -3,7 +3,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - public class MultipleChoiceElementValue: ContentElementValue> + internal class MultipleChoiceElementValue: ContentElementValue> { } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/NumberElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/NumberElementValue.cs index ab8db2a8..a55b0328 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/NumberElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/NumberElementValue.cs @@ -2,7 +2,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - public class NumberElementValue: ContentElementValue + internal class NumberElementValue: ContentElementValue { } } \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/RichTextElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/RichTextElementValue.cs index 28d82f0d..da7da5b9 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/RichTextElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/RichTextElementValue.cs @@ -5,7 +5,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - public class RichTextElementValue : StringElementValue, IRichTextElementValue + internal class RichTextElementValue : StringElementValue, IRichTextElementValue { [JsonProperty("images")] public IDictionary Images { get; set; } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs index 6773ee68..68f35928 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/StringElementValue.cs @@ -1,6 +1,6 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - public class StringElementValue : ContentElementValue + internal class StringElementValue : ContentElementValue { } } \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/TaxonomyElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/TaxonomyElementValue.cs index 6cc5f649..810b7b77 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/TaxonomyElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/TaxonomyElementValue.cs @@ -4,7 +4,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - public class TaxonomyElementValue : ContentElementValue>, ITaxonomyElementValue + internal class TaxonomyElementValue : ContentElementValue>, ITaxonomyElementValue { [JsonProperty("taxonomy_group")] public string TaxonomyGroup { get; set; } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/TextElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/TextElementValue.cs index bd379839..efc3addb 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/TextElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/TextElementValue.cs @@ -1,6 +1,6 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - public class TextElementValue : StringElementValue + internal class TextElementValue : StringElementValue { } diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/UrlSlugElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/UrlSlugElementValue.cs index dd647625..f132304c 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/UrlSlugElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/UrlSlugElementValue.cs @@ -1,6 +1,6 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - public class UrlSlugElementValue : StringElementValue + internal class UrlSlugElementValue : StringElementValue { } diff --git a/Kontent.Ai.Delivery.Extensions.Universal/DeliveryItemGenericResponse.cs b/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryItemGenericResponse.cs similarity index 76% rename from Kontent.Ai.Delivery.Extensions.Universal/DeliveryItemGenericResponse.cs rename to Kontent.Ai.Delivery/ContentItems/Universal/DeliveryItemGenericResponse.cs index f8bb9d23..d97c6f89 100644 --- a/Kontent.Ai.Delivery.Extensions.Universal/DeliveryItemGenericResponse.cs +++ b/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryItemGenericResponse.cs @@ -2,17 +2,15 @@ using Kontent.Ai.Delivery.Abstractions; using Kontent.Ai.Delivery.SharedModels; -namespace Kontent.Ai.Delivery.Extensions.Universal +namespace Kontent.Ai.Delivery.ContentItems.Universal { - internal class DeliveryUniversalItemResponse : IResponse, IDeliveryUniversalItemResponse + internal class DeliveryUniversalItemResponse : AbstractResponse, IDeliveryUniversalItemResponse { public IUniversalContentItem Item { get; } public Dictionary LinkedItems { get; } - public IApiResponse ApiResponse { get; } - - public DeliveryUniversalItemResponse(IApiResponse response) + public DeliveryUniversalItemResponse(IApiResponse response) : base(response) { ApiResponse = response; } diff --git a/Kontent.Ai.Delivery.Extensions.Universal/DeliveryUniversalItemListingResponse.cs b/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryUniversalItemListingResponse.cs similarity index 77% rename from Kontent.Ai.Delivery.Extensions.Universal/DeliveryUniversalItemListingResponse.cs rename to Kontent.Ai.Delivery/ContentItems/Universal/DeliveryUniversalItemListingResponse.cs index f5440425..4366c5f7 100644 --- a/Kontent.Ai.Delivery.Extensions.Universal/DeliveryUniversalItemListingResponse.cs +++ b/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryUniversalItemListingResponse.cs @@ -2,9 +2,9 @@ using Kontent.Ai.Delivery.Abstractions; using Kontent.Ai.Delivery.SharedModels; -namespace Kontent.Ai.Delivery.Extensions.Universal +namespace Kontent.Ai.Delivery.ContentItems.Universal { - internal class DeliveryUniversalItemListingResponse : IResponse, IDeliveryUniversalItemListingResponse + internal class DeliveryUniversalItemListingResponse : AbstractResponse, IDeliveryUniversalItemListingResponse { public IList Items { get; } @@ -13,9 +13,7 @@ internal class DeliveryUniversalItemListingResponse : IResponse, IDeliveryUniver public IPagination Pagination { get; } - public IApiResponse ApiResponse { get; } - - public DeliveryUniversalItemListingResponse(IApiResponse response) + public DeliveryUniversalItemListingResponse(IApiResponse response) : base(response) { ApiResponse = response; } diff --git a/Kontent.Ai.Delivery.Extensions.Universal/GenericModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs similarity index 65% rename from Kontent.Ai.Delivery.Extensions.Universal/GenericModelProvider.cs rename to Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs index 62155646..a940fd9e 100644 --- a/Kontent.Ai.Delivery.Extensions.Universal/GenericModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs @@ -1,19 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; using Kontent.Ai.Delivery.Abstractions; using Kontent.Ai.Delivery.ContentItems.Elements; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Kontent.Ai.Delivery.Extensions.Universal +namespace Kontent.Ai.Delivery.ContentItems.Universal { - internal class UniversalContentItemModelProvider + internal class GenericModelProvider : IUniversalItemModelProvider { - public static async Task GetContentItemGenericModelAsync(object item, JsonSerializer serializer) - => (IUniversalContentItem)await GetContentItemModelAsync((JObject)item, serializer); + internal readonly JsonSerializer Serializer; - internal static async Task GetContentItemModelAsync(JObject serializedItem, JsonSerializer serializer) + internal GenericModelProvider(JsonSerializer serializer) + { + Serializer = serializer; + } + + public async Task GetContentItemGenericModelAsync(object item) + => (IUniversalContentItem)await GetContentItemModelAsync((JObject)item); + + internal async Task GetContentItemModelAsync(JObject serializedItem) { var result = new UniversalContentItem() { - System = serializedItem?["system"]?.ToObject(serializer) + System = serializedItem?["system"]?.ToObject(Serializer) }; foreach (var item in (JObject)serializedItem["elements"]) @@ -26,18 +36,18 @@ internal static async Task GetContentItemModelAsync(JObje IContentElementValue value = (element["type"].ToString()) switch { // TODO do we want to use string/structured data for rich text - probably think about support both ways - "rich_text" => element.ToObject(serializer).WithCodename(key), - "asset" => element.ToObject(serializer).WithCodename(key), - "number" => element.ToObject(serializer).WithCodename(key), + "rich_text" => element.ToObject(Serializer).WithCodename(key), + "asset" => element.ToObject(Serializer).WithCodename(key), + "number" => element.ToObject(Serializer).WithCodename(key), // TODO do we want to use string/structured data for date time => structured is OK - "date_time" => element.ToObject(serializer).WithCodename(key), - "multiple_choice" => element.ToObject(serializer).WithCodename(key), - "taxonomy" => element.ToObject(serializer).WithCodename(key), + "date_time" => element.ToObject(Serializer).WithCodename(key), + "multiple_choice" => element.ToObject(Serializer).WithCodename(key), + "taxonomy" => element.ToObject(Serializer).WithCodename(key), // TODO what Linked items + what SubPages? - "modular_content" => element.ToObject(serializer).WithCodename(key), - "custom" => element.ToObject(serializer).WithCodename(key), - "url_slug" => element.ToObject(serializer).WithCodename(key), - "text" => element.ToObject(serializer).WithCodename(key), + "modular_content" => element.ToObject(Serializer).WithCodename(key), + "custom" => element.ToObject(Serializer).WithCodename(key), + "url_slug" => element.ToObject(Serializer).WithCodename(key), + "text" => element.ToObject(Serializer).WithCodename(key), _ => throw new ArgumentException($"Argument type ({element["type"].ToString()}) not supported.") }; result.Elements.Add(key, value); diff --git a/Kontent.Ai.Delivery.Extensions.Universal/UniversalContentItem.cs b/Kontent.Ai.Delivery/ContentItems/Universal/UniversalContentItem.cs similarity index 86% rename from Kontent.Ai.Delivery.Extensions.Universal/UniversalContentItem.cs rename to Kontent.Ai.Delivery/ContentItems/Universal/UniversalContentItem.cs index e4a00e2b..f0f7ca4d 100644 --- a/Kontent.Ai.Delivery.Extensions.Universal/UniversalContentItem.cs +++ b/Kontent.Ai.Delivery/ContentItems/Universal/UniversalContentItem.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Kontent.Ai.Delivery.Abstractions; -namespace Kontent.Ai.Delivery.Extensions.Universal +namespace Kontent.Ai.Delivery.ContentItems.Universal { public class UniversalContentItem : IUniversalContentItem { diff --git a/Kontent.Ai.Delivery/ContentTypes/DeliveryElementResponse.cs b/Kontent.Ai.Delivery/ContentTypes/DeliveryElementResponse.cs index 0d032d90..3f9e8129 100644 --- a/Kontent.Ai.Delivery/ContentTypes/DeliveryElementResponse.cs +++ b/Kontent.Ai.Delivery/ContentTypes/DeliveryElementResponse.cs @@ -22,7 +22,7 @@ public IContentElement Element /// The response from Kontent.ai Delivery API that contains a content element. /// A content element. [JsonConstructor] - internal DeliveryElementResponse(IApiResponse response, IContentElement element) : base(response) + internal DeliveryElementResponse(ApiResponse response, IContentElement element) : base(response) { Element = element; } @@ -31,7 +31,7 @@ internal DeliveryElementResponse(IApiResponse response, IContentElement element) /// Initializes a new instance of the class. /// /// The response from Kontent.ai Delivery API that contains a content element. - internal DeliveryElementResponse(IApiResponse response) : base(response) + internal DeliveryElementResponse(ApiResponse response) : base(response) { } } diff --git a/Kontent.Ai.Delivery/ContentTypes/DeliveryTypeListingResponse.cs b/Kontent.Ai.Delivery/ContentTypes/DeliveryTypeListingResponse.cs index a99f63a6..fe7ad63a 100644 --- a/Kontent.Ai.Delivery/ContentTypes/DeliveryTypeListingResponse.cs +++ b/Kontent.Ai.Delivery/ContentTypes/DeliveryTypeListingResponse.cs @@ -27,7 +27,7 @@ public IList Types /// A collection of content types. /// Response paging information. [JsonConstructor] - internal DeliveryTypeListingResponse(IApiResponse response, IList types, IPagination pagination) : base(response) + internal DeliveryTypeListingResponse(ApiResponse response, IList types, IPagination pagination) : base(response) { Types = types; Pagination = pagination; @@ -37,7 +37,7 @@ internal DeliveryTypeListingResponse(IApiResponse response, IList /// Initializes a new instance of the class. /// /// The response from Kontent.ai Delivery API that contains content types. - internal DeliveryTypeListingResponse(IApiResponse response) : base(response) + internal DeliveryTypeListingResponse(ApiResponse response) : base(response) { } } diff --git a/Kontent.Ai.Delivery/ContentTypes/DeliveryTypeResponse.cs b/Kontent.Ai.Delivery/ContentTypes/DeliveryTypeResponse.cs index 82799bca..179d1396 100644 --- a/Kontent.Ai.Delivery/ContentTypes/DeliveryTypeResponse.cs +++ b/Kontent.Ai.Delivery/ContentTypes/DeliveryTypeResponse.cs @@ -19,7 +19,7 @@ public IContentType Type /// The response from Kontent.ai Delivery API that contains a content type. /// A content type. [JsonConstructor] - internal DeliveryTypeResponse(IApiResponse response, IContentType type) : base(response) + internal DeliveryTypeResponse(ApiResponse response, IContentType type) : base(response) { Type = type; } @@ -28,7 +28,7 @@ internal DeliveryTypeResponse(IApiResponse response, IContentType type) : base(r /// Initializes a new instance of the class. /// /// The response from Kontent.ai Delivery API that contains a content type. - internal DeliveryTypeResponse(IApiResponse response) : base(response) + internal DeliveryTypeResponse(ApiResponse response) : base(response) { } } diff --git a/Kontent.Ai.Delivery/DeliveryClient.cs b/Kontent.Ai.Delivery/DeliveryClient.cs index 92faf33a..224829fa 100644 --- a/Kontent.Ai.Delivery/DeliveryClient.cs +++ b/Kontent.Ai.Delivery/DeliveryClient.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Kontent.Ai.Delivery.Abstractions; using Kontent.Ai.Delivery.ContentItems; +using Kontent.Ai.Delivery.ContentItems.Universal; using Kontent.Ai.Delivery.ContentTypes; using Kontent.Ai.Delivery.Extensions; using Kontent.Ai.Delivery.Languages; @@ -30,20 +31,17 @@ internal sealed class DeliveryClient : IDeliveryClient { private DeliveryEndpointUrlBuilder _urlBuilder; - public readonly IOptionsMonitor DeliveryOptions; + internal readonly IOptionsMonitor DeliveryOptions; internal readonly IModelProvider ModelProvider; internal readonly ITypeProvider TypeProvider; internal readonly IRetryPolicyProvider RetryPolicyProvider; internal readonly IDeliveryHttpClient DeliveryHttpClient; - public readonly JsonSerializer Serializer; + internal readonly JsonSerializer Serializer; internal readonly ILoggerFactory LoggerFactory; - public DeliveryEndpointUrlBuilder UrlBuilder - => _urlBuilder ??= new DeliveryEndpointUrlBuilder(DeliveryOptions); - - - IOptionsMonitor IDeliveryClient.DeliveryOptions => throw new NotImplementedException(); + internal readonly IUniversalItemModelProvider GenericModelProvider; - object IDeliveryClient.Serializer => (object)Serializer; + internal DeliveryEndpointUrlBuilder UrlBuilder + => _urlBuilder ??= new DeliveryEndpointUrlBuilder(DeliveryOptions); /// /// Initializes a new instance of the class for retrieving content of the specified project. @@ -63,7 +61,8 @@ public DeliveryClient( IDeliveryHttpClient deliveryHttpClient = null, JsonSerializer serializer = null, // TODO why logger factory is not everywhere ? - ILoggerFactory loggerFactory = null) + ILoggerFactory loggerFactory = null, + IUniversalItemModelProvider genericModelProvider = null) { DeliveryOptions = deliveryOptions; ModelProvider = modelProvider; @@ -72,6 +71,8 @@ public DeliveryClient( DeliveryHttpClient = deliveryHttpClient; Serializer = serializer; LoggerFactory = loggerFactory; + // TODO IOC? Default? Check references + GenericModelProvider = new GenericModelProvider(serializer); } /// @@ -96,7 +97,7 @@ public async Task> GetItemAsync(string codename, IEn return new DeliveryItemResponse(response); } - var content = (JObject)(await response.GetJsonContentAsync()); + var content = await response.GetJsonContentAsync(); var model = await ModelProvider.GetContentItemModelAsync(content?["item"], content?["modular_content"]); return new DeliveryItemResponse(response, model); } @@ -118,7 +119,7 @@ public async Task> GetItemsAsync(IEnumerable< return new DeliveryItemListingResponse(response); } - var content = (JObject)await response.GetJsonContentAsync(); + var content = await response.GetJsonContentAsync(); var pagination = content["pagination"].ToObject(Serializer); var items = ((JArray)content["items"]).Select(async source => await ModelProvider.GetContentItemModelAsync(source, content["modular_content"])); @@ -147,7 +148,7 @@ async Task> GetItemsBatchAsync(string continuationT return new DeliveryItemsFeedResponse(response); } - var content = (JObject)await response.GetJsonContentAsync(); + var content = await response.GetJsonContentAsync(); var items = ((JArray)content["items"]).Select(async source => await ModelProvider.GetContentItemModelAsync(source, content["modular_content"])); @@ -180,7 +181,7 @@ public async Task GetTypeAsync(string codename) return new DeliveryTypeResponse(response); } - var type = ((JObject)await response.GetJsonContentAsync())?.ToObject(Serializer); + var type = (await response.GetJsonContentAsync())?.ToObject(Serializer); return new DeliveryTypeResponse(response, type); } @@ -200,8 +201,7 @@ public async Task GetTypesAsync(IEnumerable(Serializer); var types = content["types"].ToObject>(Serializer); return new DeliveryTypeListingResponse(response, types.ToList(), pagination); @@ -243,7 +243,7 @@ public async Task GetContentElementAsync(string conten return new DeliveryElementResponse(response); } - var content = (JObject)await response.GetJsonContentAsync(); + var content = await response.GetJsonContentAsync(); var element = content?.ToObject(Serializer); return new DeliveryElementResponse(response, element); } @@ -273,7 +273,7 @@ public async Task GetTaxonomyAsync(string codename) return new DeliveryTaxonomyResponse(response); } - var taxonomy = ((JObject)await response.GetJsonContentAsync())?.ToObject(Serializer); + var taxonomy = (await response.GetJsonContentAsync())?.ToObject(Serializer); return new DeliveryTaxonomyResponse(response, taxonomy); } @@ -292,7 +292,7 @@ public async Task GetTaxonomiesAsync(IEnumerab return new DeliveryTaxonomyListingResponse(response); } - var content = (JObject)await response.GetJsonContentAsync(); + var content = await response.GetJsonContentAsync(); var pagination = content["pagination"].ToObject(Serializer); var taxonomies = content["taxonomies"].ToObject>(Serializer); return new DeliveryTaxonomyListingResponse(response, taxonomies.ToList(), pagination); @@ -313,13 +313,81 @@ public async Task GetLanguagesAsync(IEnumerabl return new DeliveryLanguageListingResponse(response); } - var content = (JObject)await response.GetJsonContentAsync(); + var content = await response.GetJsonContentAsync(); var pagination = content["pagination"].ToObject(Serializer); var languages = content["languages"].ToObject>(Serializer); return new DeliveryLanguageListingResponse(response, languages.ToList(), pagination); } - public async Task GetDeliveryResponseAsync(string endpointUrl, string continuationToken = null) + + public async Task GetUniversalItemAsync(string codename, IEnumerable parameters = null) + { + if (string.IsNullOrEmpty(codename)) + { + throw new ArgumentException("Entered item codename is not valid.", nameof(codename)); + } + + var endpointUrl = UrlBuilder.GetItemUrl(codename, parameters); + var response = await GetDeliveryResponseAsync(endpointUrl); + + if (!response.IsSuccess) + { + return new DeliveryUniversalItemResponse(response); + } + + var content = await response.GetJsonContentAsync(); + var model = await GenericModelProvider.GetContentItemGenericModelAsync(content["item"]); + + var linkedUniversalItems = await Task.WhenAll( + content["modular_content"]? + .Values() + .Select(async linkedItem => + { + var model = await GenericModelProvider.GetContentItemGenericModelAsync(linkedItem); + return new KeyValuePair(model.System.Codename, model); + }) + ); + + return new DeliveryUniversalItemResponse( + response, + model, + linkedUniversalItems.ToDictionary(pair => pair.Key, pair => pair.Value)); + } + + public async Task GetUniversalItemsAsync(IEnumerable parameters = null) + { + var endpointUrl = UrlBuilder.GetItemsUrl(parameters); + var response = await GetDeliveryResponseAsync(endpointUrl); + + if (!response.IsSuccess) + { + return new DeliveryUniversalItemListingResponse(response); + } + + var content = await response.GetJsonContentAsync(); + var pagination = content["pagination"].ToObject(Serializer); + + var items = ((JArray)content["items"]).Select(async source => await GenericModelProvider.GetContentItemGenericModelAsync(source)); + + var linkedUniversalItems = await Task.WhenAll( + content["modular_content"]? + .Values() + .Select(async linkedItem => + { + var model = await GenericModelProvider.GetContentItemGenericModelAsync(linkedItem); + return new KeyValuePair(model.System.Codename, model); + }) + ); + + return new DeliveryUniversalItemListingResponse( + response, + (await Task.WhenAll(items)).ToList(), + pagination, + linkedUniversalItems.ToDictionary(pair => pair.Key, pair => pair.Value) + ); + } + + private async Task GetDeliveryResponseAsync(string endpointUrl, string continuationToken = null) { if (DeliveryOptions.CurrentValue.UsePreviewApi && DeliveryOptions.CurrentValue.UseSecureAccess) { diff --git a/Kontent.Ai.Delivery/Languages/DeliveryLanguageListingResponse.cs b/Kontent.Ai.Delivery/Languages/DeliveryLanguageListingResponse.cs index e85f536a..6b4d4781 100644 --- a/Kontent.Ai.Delivery/Languages/DeliveryLanguageListingResponse.cs +++ b/Kontent.Ai.Delivery/Languages/DeliveryLanguageListingResponse.cs @@ -27,7 +27,7 @@ public IPagination Pagination /// A collection of languages. /// Response paging information. [JsonConstructor] - internal DeliveryLanguageListingResponse(IApiResponse response, IList languages, IPagination pagination) : base(response) + internal DeliveryLanguageListingResponse(ApiResponse response, IList languages, IPagination pagination) : base(response) { Languages = languages; Pagination = pagination; @@ -37,7 +37,7 @@ internal DeliveryLanguageListingResponse(IApiResponse response, IList /// Initializes a new instance of the class. /// /// The response from Kontent.ai Delivery API that contains languages. - internal DeliveryLanguageListingResponse(IApiResponse response) : base(response) + internal DeliveryLanguageListingResponse(ApiResponse response) : base(response) { } } diff --git a/Kontent.Ai.Delivery/SharedModels/ApiResponse.cs b/Kontent.Ai.Delivery/SharedModels/ApiResponse.cs index 2105c672..c3a961b9 100644 --- a/Kontent.Ai.Delivery/SharedModels/ApiResponse.cs +++ b/Kontent.Ai.Delivery/SharedModels/ApiResponse.cs @@ -13,7 +13,7 @@ namespace Kontent.Ai.Delivery.SharedModels /// Represents a successful JSON response from Kontent.ai Delivery API. /// [DebuggerDisplay("Url = {" + nameof(RequestUrl) + "}")] - public sealed class ApiResponse : IApiResponse + internal sealed class ApiResponse : IApiResponse { private JObject _jsonContent; private string _content; @@ -48,7 +48,7 @@ public string Content /// public IError Error { get; } - + /// /// Initializes a new instance of the class. /// @@ -56,7 +56,7 @@ public string Content /// Specifies whether content is stale. /// Continuation token to be used for continuing enumeration. /// URL used to retrieve this response. - internal ApiResponse(HttpContent httpContent, bool hasStaleContent, string continuationToken, string requestUrl) : + internal ApiResponse(HttpContent httpContent, bool hasStaleContent, string continuationToken, string requestUrl) : this(httpContent, hasStaleContent, continuationToken, requestUrl, null) { } @@ -101,10 +101,5 @@ public async Task GetJsonContentAsync() } return _jsonContent; } - - async Task IApiResponse.GetJsonContentAsync() - { - return await Task.FromResult((object)GetJsonContentAsync()); - } } } diff --git a/Kontent.Ai.Delivery/SharedModels/Pagination.cs b/Kontent.Ai.Delivery/SharedModels/Pagination.cs index 23876ce2..a0196db3 100644 --- a/Kontent.Ai.Delivery/SharedModels/Pagination.cs +++ b/Kontent.Ai.Delivery/SharedModels/Pagination.cs @@ -6,7 +6,7 @@ namespace Kontent.Ai.Delivery.SharedModels { /// [DebuggerDisplay("Count = {" + nameof(Count) + "}, Total = {" + nameof(TotalCount) + "}")] - public sealed class Pagination : IPagination + internal sealed class Pagination : IPagination { /// public int Skip { get; } diff --git a/Kontent.Ai.Delivery/TaxonomyGroups/DeliveryTaxonomyListingResponse.cs b/Kontent.Ai.Delivery/TaxonomyGroups/DeliveryTaxonomyListingResponse.cs index 63f83a50..1799c324 100644 --- a/Kontent.Ai.Delivery/TaxonomyGroups/DeliveryTaxonomyListingResponse.cs +++ b/Kontent.Ai.Delivery/TaxonomyGroups/DeliveryTaxonomyListingResponse.cs @@ -33,7 +33,7 @@ public IList Taxonomies /// A collection of taxonomies. /// Response paging information. [JsonConstructor] - internal DeliveryTaxonomyListingResponse(IApiResponse response, IList taxonomies, IPagination pagination) : base(response) + internal DeliveryTaxonomyListingResponse(ApiResponse response, IList taxonomies, IPagination pagination) : base(response) { Taxonomies = taxonomies; Pagination = pagination; @@ -43,7 +43,7 @@ internal DeliveryTaxonomyListingResponse(IApiResponse response, IList class. /// /// The response from Kontent.ai Delivery API that contains taxonomies. - internal DeliveryTaxonomyListingResponse(IApiResponse response) : base(response) + internal DeliveryTaxonomyListingResponse(ApiResponse response) : base(response) { } } diff --git a/Kontent.Ai.Delivery/TaxonomyGroups/DeliveryTaxonomyResponse.cs b/Kontent.Ai.Delivery/TaxonomyGroups/DeliveryTaxonomyResponse.cs index 221a7a2b..b2f7a690 100644 --- a/Kontent.Ai.Delivery/TaxonomyGroups/DeliveryTaxonomyResponse.cs +++ b/Kontent.Ai.Delivery/TaxonomyGroups/DeliveryTaxonomyResponse.cs @@ -20,7 +20,7 @@ public ITaxonomyGroup Taxonomy /// The response from Kontent.ai Delivery API that contains a taxonomy group. /// A taxonomy group. [JsonConstructor] - internal DeliveryTaxonomyResponse(IApiResponse response, ITaxonomyGroup taxonomy) : base(response) + internal DeliveryTaxonomyResponse(ApiResponse response, ITaxonomyGroup taxonomy) : base(response) { Taxonomy = taxonomy; } @@ -29,7 +29,7 @@ internal DeliveryTaxonomyResponse(IApiResponse response, ITaxonomyGroup taxonomy /// Initializes a new instance of the class. /// /// The response from Kontent.ai Delivery API that contains a taxonomy group. - internal DeliveryTaxonomyResponse(IApiResponse response) : base(response) + internal DeliveryTaxonomyResponse(ApiResponse response) : base(response) { } } From f2305e7d6b520e6e66dee693cb65d302449c40ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Thu, 30 Mar 2023 20:56:52 +0200 Subject: [PATCH 16/34] implement delivery client cache + add happy path test --- .../DeliveryClientCacheTests.cs | 59 +++++++++++++++++++ .../DeliveryClientCache.cs | 18 ++++-- .../DeliveryUniversalItemListingResponse.cs | 6 +- ...se.cs => DeliveryUniversalItemResponse.cs} | 6 +- 4 files changed, 81 insertions(+), 8 deletions(-) rename Kontent.Ai.Delivery/ContentItems/Universal/{DeliveryItemGenericResponse.cs => DeliveryUniversalItemResponse.cs} (61%) diff --git a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs index 72cfb388..bb7ffff9 100644 --- a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs +++ b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs @@ -731,5 +731,64 @@ public async Task GetItemTypedAsync_BrokenCache_Crash_ThrowsException(CacheTypeE } #endregion + + #region GetUniversalItemAsync + + [Theory] + [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Absolute)] + [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Sliding)] + [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Absolute)] + [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Sliding)] + public async Task GetUniversalItemAsync_ResponseIsCached(CacheTypeEnum cacheType, CacheExpirationType cacheExpirationType) + { + const string codename = "codename"; + var url = $"items/{codename}"; + var item = CreateItemResponse(CreateItem(codename, "original")); + var updatedItem = CreateItemResponse(CreateItem(codename, "updated")); + var scenarioBuilder = new ScenarioBuilder(cacheType, cacheExpirationType); + var scenario = scenarioBuilder.WithResponse(url, item).Build(); + + var firstResponse = await scenario.CachingClient.GetUniversalItemAsync(codename); + scenario = scenarioBuilder.WithResponse(url, updatedItem).Build(); + + var secondResponse = await scenario.CachingClient.GetUniversalItemAsync(codename); + //Check + firstResponse.Should().NotBeNull(); + firstResponse.Should().BeEquivalentTo(secondResponse); + scenario.GetRequestCount(url).Should().Be(1); + } + + // TODO Add all test to mirror strongly type responses + + #endregion + + #region GetUniversalItemsAsync + + [Theory] + [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Absolute)] + [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Sliding)] + [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Absolute)] + [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Sliding)] + public async Task GetUniversalItemsAsync_ResponseIsCached(CacheTypeEnum cacheType, CacheExpirationType cacheExpirationType) + { + var url = "items"; + var itemB = CreateItem("b", "original"); + var items = CreateItemsResponse(new[] { CreateItem("a", "original"), itemB }); + var updatedItems = CreateItemsResponse(new[] { CreateItem("a", "updated"), itemB }); + var scenarioBuilder = new ScenarioBuilder(cacheType, cacheExpirationType); + var scenario = scenarioBuilder.WithResponse(url, items).Build(); + + var firstResponse = await scenario.CachingClient.GetUniversalItemsAsync(); + scenario = scenarioBuilder.WithResponse(url, updatedItems).Build(); + var secondResponse = await scenario.CachingClient.GetUniversalItemsAsync(); + //Check + firstResponse.Should().NotBeNull(); + firstResponse.Should().BeEquivalentTo(secondResponse); + scenario.GetRequestCount(url).Should().Be(1); + } + + // TODO Add all test to mirror strongly type responses + + #endregion } } diff --git a/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs b/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs index 314a39ff..bafe51b8 100644 --- a/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs +++ b/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs @@ -158,15 +158,25 @@ public async Task GetLanguagesAsync(IEnumerabl } // TODO - public Task GetUniversalItemAsync(string codename, IEnumerable parameters = null) + public async Task GetUniversalItemAsync(string codename, IEnumerable parameters = null) { - throw new NotImplementedException(); + var queryParameters = parameters?.ToList(); + return await _deliveryCacheManager.GetOrAddAsync( + CacheHelpers.GetItemKey(codename, queryParameters), + () => _deliveryClient.GetUniversalItemAsync(codename, queryParameters), + response => response != null, + CacheHelpers.GetItemDependencies); } // TODO - public Task GetUniversalItemsAsync(IEnumerable parameters = null) + public async Task GetUniversalItemsAsync(IEnumerable parameters = null) { - throw new NotImplementedException(); + var queryParameters = parameters?.ToList(); + return await _deliveryCacheManager.GetOrAddAsync( + CacheHelpers.GetItemsKey>(queryParameters), + () => _deliveryClient.GetUniversalItemsAsync(queryParameters), + response => response.Items.Any(), + CacheHelpers.GetItemsDependencies); } } } diff --git a/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryUniversalItemListingResponse.cs b/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryUniversalItemListingResponse.cs index 4366c5f7..f8c91f49 100644 --- a/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryUniversalItemListingResponse.cs +++ b/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryUniversalItemListingResponse.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; using Kontent.Ai.Delivery.Abstractions; using Kontent.Ai.Delivery.SharedModels; +using Newtonsoft.Json; namespace Kontent.Ai.Delivery.ContentItems.Universal { - internal class DeliveryUniversalItemListingResponse : AbstractResponse, IDeliveryUniversalItemListingResponse + internal sealed class DeliveryUniversalItemListingResponse : AbstractResponse, IDeliveryUniversalItemListingResponse { public IList Items { get; } @@ -18,7 +19,8 @@ public DeliveryUniversalItemListingResponse(IApiResponse response) : base(respon ApiResponse = response; } - public DeliveryUniversalItemListingResponse(IApiResponse response, IList items, IPagination pagination, Dictionary linkedItems = null) : this(response) + [JsonConstructor] + internal DeliveryUniversalItemListingResponse(IApiResponse response, IList items, IPagination pagination, Dictionary linkedItems = null) : this(response) { Items = items; Pagination = pagination; diff --git a/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryItemGenericResponse.cs b/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryUniversalItemResponse.cs similarity index 61% rename from Kontent.Ai.Delivery/ContentItems/Universal/DeliveryItemGenericResponse.cs rename to Kontent.Ai.Delivery/ContentItems/Universal/DeliveryUniversalItemResponse.cs index d97c6f89..ccbb8c41 100644 --- a/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryItemGenericResponse.cs +++ b/Kontent.Ai.Delivery/ContentItems/Universal/DeliveryUniversalItemResponse.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; using Kontent.Ai.Delivery.Abstractions; using Kontent.Ai.Delivery.SharedModels; +using Newtonsoft.Json; namespace Kontent.Ai.Delivery.ContentItems.Universal { - internal class DeliveryUniversalItemResponse : AbstractResponse, IDeliveryUniversalItemResponse + internal sealed class DeliveryUniversalItemResponse : AbstractResponse, IDeliveryUniversalItemResponse { public IUniversalContentItem Item { get; } @@ -15,7 +16,8 @@ public DeliveryUniversalItemResponse(IApiResponse response) : base(response) ApiResponse = response; } - public DeliveryUniversalItemResponse(IApiResponse response, IUniversalContentItem item, Dictionary linkedItems = null) : this(response) + [JsonConstructor] + internal DeliveryUniversalItemResponse(IApiResponse response, IUniversalContentItem item, Dictionary linkedItems = null) : this(response) { Item = item; LinkedItems = linkedItems; From a08bd4251eecc6d2f012ac7507aae3951a966872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Fri, 31 Mar 2023 13:43:05 +0200 Subject: [PATCH 17/34] implement chache dependencies handling for universal items --- .../DeliveryClientCacheTests.cs | 144 ++++++++++++++++++ Kontent.Ai.Delivery.Caching/CacheHelpers.cs | 8 +- 2 files changed, 150 insertions(+), 2 deletions(-) diff --git a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs index bb7ffff9..0d585158 100644 --- a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs +++ b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs @@ -36,6 +36,7 @@ public async Task GetItemTypedAsync_ResponseIsCached(CacheTypeEnum cacheType, Ca firstResponse.Should().BeEquivalentTo(secondResponse); scenario.GetRequestCount(url).Should().Be(1); } + [Theory] [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Absolute)] [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Sliding)] @@ -760,6 +761,149 @@ public async Task GetUniversalItemAsync_ResponseIsCached(CacheTypeEnum cacheType // TODO Add all test to mirror strongly type responses + [Theory] + [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Absolute)] + [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Sliding)] + public async Task GetUniversalItemAsync_InvalidatedByItemDependency(CacheTypeEnum cacheType, CacheExpirationType cacheExpirationType) + { + const string codename = "codename"; + var url = $"items/{codename}"; + var item = CreateItemResponse(CreateItem(codename, "original")); + var updatedItem = CreateItemResponse(CreateItem(codename, "updated")); + var scenarioBuilder = new ScenarioBuilder(cacheType, cacheExpirationType); + var scenario = scenarioBuilder.WithResponse(url, item).Build(); + + var firstResponse = await scenario.CachingClient.GetUniversalItemAsync(codename); + scenario = scenarioBuilder.WithResponse(url, updatedItem).Build(); + scenario.InvalidateDependency(CacheHelpers.GetItemDependencyKey(codename)); + var secondResponse = await scenario.CachingClient.GetUniversalItemAsync(codename); + + //Check + firstResponse.Should().NotBeNull(); + secondResponse.Should().NotBeNull(); + firstResponse.Should().NotBeEquivalentTo(secondResponse); + scenario.GetRequestCount(url).Should().Be(2); + } + + // [Theory] + // [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Absolute)] + // [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Sliding)] + // [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Absolute)] + // [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Sliding)] + // public async Task GetItemTypedAsync_InvalidatedByItemKey(CacheTypeEnum cacheType, CacheExpirationType cacheExpirationType) + // { + // const string codename = "codename"; + // var url = $"items/{codename}"; + // var item = CreateItemResponse(CreateItem(codename, "original")); + // var updatedItem = CreateItemResponse(CreateItem(codename, "updated")); + // var scenarioBuilder = new ScenarioBuilder(cacheType, cacheExpirationType); + // var scenario = scenarioBuilder.WithResponse(url, item).Build(); + // var firstResponse = await scenario.CachingClient.GetItemAsync(codename); + // scenario = scenarioBuilder.WithResponse(url, updatedItem).Build(); + // scenario.InvalidateDependency(CacheHelpers.GetItemKey(codename, Enumerable.Empty())); + // var secondResponse = await scenario.CachingClient.GetItemAsync(codename); + // //Check + // firstResponse.Should().NotBeNull(); + // secondResponse.Should().NotBeNull(); + // firstResponse.Should().NotBeEquivalentTo(secondResponse); + // scenario.GetRequestCount(url).Should().Be(2); + // } + + // [Theory] + // [InlineData(CacheExpirationType.Absolute)] + // [InlineData(CacheExpirationType.Sliding)] + // public async Task GetItemTypedAsync_InvalidatedByLinkedItemDependency(CacheExpirationType cacheExpirationType) + // { + // const string codename = "codename"; + // var url = $"items/{codename}"; + // const string modularCodename = "modular_codename"; + // var modularContent = new[] { (modularCodename, CreateItem(modularCodename)) }; + // var item = CreateItemResponse(CreateItem(codename, "original"), modularContent); + // var updatedItem = CreateItemResponse(CreateItem(codename, "updated"), modularContent); + // var scenarioBuilder = new ScenarioBuilder(cacheExpirationType: cacheExpirationType); + // var scenario = scenarioBuilder.WithResponse(url, item).Build(); + // var firstResponse = await scenario.CachingClient.GetItemAsync(codename); + // scenario = scenarioBuilder.WithResponse(url, updatedItem).Build(); + // scenario.InvalidateDependency(CacheHelpers.GetItemDependencyKey(modularCodename)); + // var secondResponse = await scenario.CachingClient.GetItemAsync(codename); + // //Check + // firstResponse.Should().NotBeNull(); + // secondResponse.Should().NotBeNull(); + // firstResponse.Should().NotBeEquivalentTo(secondResponse); + // scenario.GetRequestCount(url).Should().Be(2); + // } + + // [Theory] + // [InlineData(CacheExpirationType.Absolute)] + // [InlineData(CacheExpirationType.Sliding)] + // public async Task GetItemTypedAsync_NotInvalidatedByComponentDependency(CacheExpirationType cacheExpirationType) + // { + // const string codename = "codename"; + // var url = $"items/{codename}"; + // var component = CreateComponent(); + // var modularContent = new[] { component }; + // var item = CreateItemResponse(CreateItem(codename, "original"), modularContent); + // var updatedItem = CreateItemResponse(CreateItem(codename, "updated"), modularContent); + // var scenarioBuilder = new ScenarioBuilder(cacheExpirationType: cacheExpirationType); + // var scenario = scenarioBuilder.WithResponse(url, item).Build(); + // var firstResponse = await scenario.CachingClient.GetItemAsync(codename); + // scenario = scenarioBuilder.WithResponse(url, updatedItem).Build(); + // scenario.InvalidateDependency(CacheHelpers.GetItemDependencyKey(component.codename)); + // var secondResponse = await scenario.CachingClient.GetItemAsync(codename); + // //Check + // firstResponse.Should().NotBeNull(); + // firstResponse.Should().BeEquivalentTo(secondResponse); + // scenario.GetRequestCount(url).Should().Be(1); + // } + + // [Theory] + // [InlineData(CacheExpirationType.Absolute)] + // [InlineData(CacheExpirationType.Sliding)] + // public async Task GetItemTypedAsync_TooManyItems_InvalidatedByItemsDependency(CacheExpirationType cacheExpirationType) + // { + // const string codename = "codename"; + // var url = $"items/{codename}"; + // var modularContent = Enumerable.Range(1, 51).Select(i => $"modular_{i}").Select(cn => (cn, CreateItem(cn))).ToList(); + // var item = CreateItemResponse(CreateItem(codename, "original"), modularContent); + // var updatedItem = CreateItemResponse(CreateItem(codename, "updated"), modularContent); + // var scenarioBuilder = new ScenarioBuilder(cacheExpirationType: cacheExpirationType); + // var scenario = scenarioBuilder.WithResponse(url, item).Build(); + // var firstResponse = await scenario.CachingClient.GetItemAsync(codename); + // scenario = scenarioBuilder.WithResponse(url, updatedItem).Build(); + // scenario.InvalidateDependency(CacheHelpers.GetItemsDependencyKey()); + // var secondResponse = await scenario.CachingClient.GetItemAsync(codename); + // //Check + // firstResponse.Should().NotBeNull(); + // secondResponse.Should().NotBeNull(); + // firstResponse.Should().NotBeEquivalentTo(secondResponse); + // scenario.GetRequestCount(url).Should().Be(2); + // } + + // [Theory] + // [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Absolute)] + // [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Sliding)] + // [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Absolute)] + // [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Sliding)] + // public async Task GetItemTypedAsync_DifferentTypesAreCachedSeparately(CacheTypeEnum cacheType, CacheExpirationType cacheExpirationType) + // { + // const string codename = "codename"; + // var url = $"items/{codename}"; + // var item = CreateItemResponse(CreateItem(codename, "original")); + // var scenarioBuilder = new ScenarioBuilder(cacheType, cacheExpirationType); + // var scenario = scenarioBuilder.WithResponse(url, item).Build(); + // var firstResponse = await scenario.CachingClient.GetItemAsync(codename); + // var secondResponse = await scenario.CachingClient.GetItemAsync(codename); + // var repeatedFirstResponse = await scenario.CachingClient.GetItemAsync(codename); + // var repeatedSecondResponse = await scenario.CachingClient.GetItemAsync(codename); + // //Check + // firstResponse.Should().NotBeNull(); + // firstResponse.Should().BeEquivalentTo(repeatedFirstResponse); + // secondResponse.Should().NotBeNull(); + // secondResponse.Should().BeEquivalentTo(repeatedSecondResponse); + // firstResponse.Should().NotBeEquivalentTo(secondResponse); + // scenario.GetRequestCount(url).Should().Be(2); + // } + #endregion #region GetUniversalItemsAsync diff --git a/Kontent.Ai.Delivery.Caching/CacheHelpers.cs b/Kontent.Ai.Delivery.Caching/CacheHelpers.cs index 018c930e..3468da2d 100644 --- a/Kontent.Ai.Delivery.Caching/CacheHelpers.cs +++ b/Kontent.Ai.Delivery.Caching/CacheHelpers.cs @@ -197,6 +197,7 @@ public static IEnumerable GetItemDependencies(IResponse response) { var dependencies = new HashSet(); + // TODO for univeral dependencies if (!IsItemResponse(response)) { return dependencies; @@ -320,8 +321,11 @@ public static IEnumerable GetLanguagesDependencies(IDeliveryLanguageList private static bool IsItemResponse(IResponse response) { - return response.GetType().IsGenericType - && IsAssignableToGenericType(response.GetType(), typeof(IDeliveryItemResponse<>)); + return + ( + response.GetType().IsGenericType + && IsAssignableToGenericType(response.GetType(), typeof(IDeliveryItemResponse<>)) + ) || typeof(IDeliveryUniversalItemResponse).IsAssignableFrom(response.GetType()); } private static bool IsAssignableToGenericType(Type givenType, Type genericType) From 2b20e6519f127a0c6bac60d223c4548908bf3b4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Fri, 31 Mar 2023 13:50:27 +0200 Subject: [PATCH 18/34] prepare docs sceleton --- .../universal-item-retrieval.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docs/retrieving-data/universal-item-retrieval.md diff --git a/docs/retrieving-data/universal-item-retrieval.md b/docs/retrieving-data/universal-item-retrieval.md new file mode 100644 index 00000000..46132fc4 --- /dev/null +++ b/docs/retrieving-data/universal-item-retrieval.md @@ -0,0 +1,24 @@ +# Universal item retrieval + +Intro + +## Retrieve data + +## Data structure + +## Convert to strongly type models + +Explain, or raise issue + +## Exceptions + +Mention `IPropertyValueConverter` and `IPropertyMapper` being excluded + +## Caching + +* Mention retrieval key `IUniversalContentItem` + `IList` + +> TODOS +> +> * [] Link from the root and in-between pages +> * [] Finish the docs \ No newline at end of file From 3e635159cd9a3921de95e8d4d92d64d86d6d86e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Fri, 31 Mar 2023 13:51:25 +0200 Subject: [PATCH 19/34] Add cache universal item by item key test --- .../DeliveryClientCacheTests.cs | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs index 0d585158..d115d7b6 100644 --- a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs +++ b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs @@ -785,29 +785,30 @@ public async Task GetUniversalItemAsync_InvalidatedByItemDependency(CacheTypeEnu scenario.GetRequestCount(url).Should().Be(2); } - // [Theory] - // [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Absolute)] - // [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Sliding)] - // [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Absolute)] - // [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Sliding)] - // public async Task GetItemTypedAsync_InvalidatedByItemKey(CacheTypeEnum cacheType, CacheExpirationType cacheExpirationType) - // { - // const string codename = "codename"; - // var url = $"items/{codename}"; - // var item = CreateItemResponse(CreateItem(codename, "original")); - // var updatedItem = CreateItemResponse(CreateItem(codename, "updated")); - // var scenarioBuilder = new ScenarioBuilder(cacheType, cacheExpirationType); - // var scenario = scenarioBuilder.WithResponse(url, item).Build(); - // var firstResponse = await scenario.CachingClient.GetItemAsync(codename); - // scenario = scenarioBuilder.WithResponse(url, updatedItem).Build(); - // scenario.InvalidateDependency(CacheHelpers.GetItemKey(codename, Enumerable.Empty())); - // var secondResponse = await scenario.CachingClient.GetItemAsync(codename); - // //Check - // firstResponse.Should().NotBeNull(); - // secondResponse.Should().NotBeNull(); - // firstResponse.Should().NotBeEquivalentTo(secondResponse); - // scenario.GetRequestCount(url).Should().Be(2); - // } + [Theory] + [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Absolute)] + [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Sliding)] + [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Absolute)] + [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Sliding)] + public async Task GetUniversalItemAsync_InvalidatedByItemKey(CacheTypeEnum cacheType, CacheExpirationType cacheExpirationType) + { + const string codename = "codename"; + var url = $"items/{codename}"; + var item = CreateItemResponse(CreateItem(codename, "original")); + var updatedItem = CreateItemResponse(CreateItem(codename, "updated")); + var scenarioBuilder = new ScenarioBuilder(cacheType, cacheExpirationType); + + var scenario = scenarioBuilder.WithResponse(url, item).Build(); + var firstResponse = await scenario.CachingClient.GetUniversalItemAsync(codename); + scenario = scenarioBuilder.WithResponse(url, updatedItem).Build(); + scenario.InvalidateDependency(CacheHelpers.GetItemKey(codename, Enumerable.Empty())); + var secondResponse = await scenario.CachingClient.GetUniversalItemAsync(codename); + //Check + firstResponse.Should().NotBeNull(); + secondResponse.Should().NotBeNull(); + firstResponse.Should().NotBeEquivalentTo(secondResponse); + scenario.GetRequestCount(url).Should().Be(2); + } // [Theory] // [InlineData(CacheExpirationType.Absolute)] From da3c08331c7c4a26336cbe2ec4541aaf882f7caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Fri, 31 Mar 2023 13:52:36 +0200 Subject: [PATCH 20/34] Add linked items dependency test --- .../DeliveryClientCacheTests.cs | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs index d115d7b6..46c160a7 100644 --- a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs +++ b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs @@ -810,29 +810,30 @@ public async Task GetUniversalItemAsync_InvalidatedByItemKey(CacheTypeEnum cache scenario.GetRequestCount(url).Should().Be(2); } - // [Theory] - // [InlineData(CacheExpirationType.Absolute)] - // [InlineData(CacheExpirationType.Sliding)] - // public async Task GetItemTypedAsync_InvalidatedByLinkedItemDependency(CacheExpirationType cacheExpirationType) - // { - // const string codename = "codename"; - // var url = $"items/{codename}"; - // const string modularCodename = "modular_codename"; - // var modularContent = new[] { (modularCodename, CreateItem(modularCodename)) }; - // var item = CreateItemResponse(CreateItem(codename, "original"), modularContent); - // var updatedItem = CreateItemResponse(CreateItem(codename, "updated"), modularContent); - // var scenarioBuilder = new ScenarioBuilder(cacheExpirationType: cacheExpirationType); - // var scenario = scenarioBuilder.WithResponse(url, item).Build(); - // var firstResponse = await scenario.CachingClient.GetItemAsync(codename); - // scenario = scenarioBuilder.WithResponse(url, updatedItem).Build(); - // scenario.InvalidateDependency(CacheHelpers.GetItemDependencyKey(modularCodename)); - // var secondResponse = await scenario.CachingClient.GetItemAsync(codename); - // //Check - // firstResponse.Should().NotBeNull(); - // secondResponse.Should().NotBeNull(); - // firstResponse.Should().NotBeEquivalentTo(secondResponse); - // scenario.GetRequestCount(url).Should().Be(2); - // } + [Theory] + [InlineData(CacheExpirationType.Absolute)] + [InlineData(CacheExpirationType.Sliding)] + public async Task GetUniversalItemAsync_InvalidatedByLinkedItemDependency(CacheExpirationType cacheExpirationType) + { + const string codename = "codename"; + var url = $"items/{codename}"; + const string modularCodename = "modular_codename"; + var modularContent = new[] { (modularCodename, CreateItem(modularCodename)) }; + var item = CreateItemResponse(CreateItem(codename, "original"), modularContent); + var updatedItem = CreateItemResponse(CreateItem(codename, "updated"), modularContent); + var scenarioBuilder = new ScenarioBuilder(cacheExpirationType: cacheExpirationType); + + var scenario = scenarioBuilder.WithResponse(url, item).Build(); + var firstResponse = await scenario.CachingClient.GetUniversalItemAsync(codename); + scenario = scenarioBuilder.WithResponse(url, updatedItem).Build(); + scenario.InvalidateDependency(CacheHelpers.GetItemDependencyKey(modularCodename)); + var secondResponse = await scenario.CachingClient.GetUniversalItemAsync(codename); + //Check + firstResponse.Should().NotBeNull(); + secondResponse.Should().NotBeNull(); + firstResponse.Should().NotBeEquivalentTo(secondResponse); + scenario.GetRequestCount(url).Should().Be(2); + } // [Theory] // [InlineData(CacheExpirationType.Absolute)] From fa1d8e0be0f8a23414f98a26e9fd9ac31fd4f74c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Fri, 31 Mar 2023 13:58:34 +0200 Subject: [PATCH 21/34] add cache dependency invalidation exception for components --- .../DeliveryClientCacheTests.cs | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs index 46c160a7..62bc8fbb 100644 --- a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs +++ b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs @@ -835,28 +835,29 @@ public async Task GetUniversalItemAsync_InvalidatedByLinkedItemDependency(CacheE scenario.GetRequestCount(url).Should().Be(2); } - // [Theory] - // [InlineData(CacheExpirationType.Absolute)] - // [InlineData(CacheExpirationType.Sliding)] - // public async Task GetItemTypedAsync_NotInvalidatedByComponentDependency(CacheExpirationType cacheExpirationType) - // { - // const string codename = "codename"; - // var url = $"items/{codename}"; - // var component = CreateComponent(); - // var modularContent = new[] { component }; - // var item = CreateItemResponse(CreateItem(codename, "original"), modularContent); - // var updatedItem = CreateItemResponse(CreateItem(codename, "updated"), modularContent); - // var scenarioBuilder = new ScenarioBuilder(cacheExpirationType: cacheExpirationType); - // var scenario = scenarioBuilder.WithResponse(url, item).Build(); - // var firstResponse = await scenario.CachingClient.GetItemAsync(codename); - // scenario = scenarioBuilder.WithResponse(url, updatedItem).Build(); - // scenario.InvalidateDependency(CacheHelpers.GetItemDependencyKey(component.codename)); - // var secondResponse = await scenario.CachingClient.GetItemAsync(codename); - // //Check - // firstResponse.Should().NotBeNull(); - // firstResponse.Should().BeEquivalentTo(secondResponse); - // scenario.GetRequestCount(url).Should().Be(1); - // } + [Theory] + [InlineData(CacheExpirationType.Absolute)] + [InlineData(CacheExpirationType.Sliding)] + public async Task GetUniversalItemAsync_NotInvalidatedByComponentDependency(CacheExpirationType cacheExpirationType) + { + const string codename = "codename"; + var url = $"items/{codename}"; + var component = CreateComponent(); + var modularContent = new[] { component }; + var item = CreateItemResponse(CreateItem(codename, "original"), modularContent); + var updatedItem = CreateItemResponse(CreateItem(codename, "updated"), modularContent); + var scenarioBuilder = new ScenarioBuilder(cacheExpirationType: cacheExpirationType); + + var scenario = scenarioBuilder.WithResponse(url, item).Build(); + var firstResponse = await scenario.CachingClient.GetUniversalItemAsync(codename); + scenario = scenarioBuilder.WithResponse(url, updatedItem).Build(); + scenario.InvalidateDependency(CacheHelpers.GetItemDependencyKey(component.codename)); + var secondResponse = await scenario.CachingClient.GetUniversalItemAsync(codename); + //Check + firstResponse.Should().NotBeNull(); + firstResponse.Should().BeEquivalentTo(secondResponse); + scenario.GetRequestCount(url).Should().Be(1); + } // [Theory] // [InlineData(CacheExpirationType.Absolute)] From ebd31212a33c6fd40e7076511117aa3cd7083f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Fri, 31 Mar 2023 14:08:09 +0200 Subject: [PATCH 22/34] add test for max cache count --- .../DeliveryClientCacheTests.cs | 45 ++++++++++--------- .../universal-item-retrieval.md | 1 + 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs index 62bc8fbb..3d18f7a2 100644 --- a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs +++ b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs @@ -859,28 +859,29 @@ public async Task GetUniversalItemAsync_NotInvalidatedByComponentDependency(Cach scenario.GetRequestCount(url).Should().Be(1); } - // [Theory] - // [InlineData(CacheExpirationType.Absolute)] - // [InlineData(CacheExpirationType.Sliding)] - // public async Task GetItemTypedAsync_TooManyItems_InvalidatedByItemsDependency(CacheExpirationType cacheExpirationType) - // { - // const string codename = "codename"; - // var url = $"items/{codename}"; - // var modularContent = Enumerable.Range(1, 51).Select(i => $"modular_{i}").Select(cn => (cn, CreateItem(cn))).ToList(); - // var item = CreateItemResponse(CreateItem(codename, "original"), modularContent); - // var updatedItem = CreateItemResponse(CreateItem(codename, "updated"), modularContent); - // var scenarioBuilder = new ScenarioBuilder(cacheExpirationType: cacheExpirationType); - // var scenario = scenarioBuilder.WithResponse(url, item).Build(); - // var firstResponse = await scenario.CachingClient.GetItemAsync(codename); - // scenario = scenarioBuilder.WithResponse(url, updatedItem).Build(); - // scenario.InvalidateDependency(CacheHelpers.GetItemsDependencyKey()); - // var secondResponse = await scenario.CachingClient.GetItemAsync(codename); - // //Check - // firstResponse.Should().NotBeNull(); - // secondResponse.Should().NotBeNull(); - // firstResponse.Should().NotBeEquivalentTo(secondResponse); - // scenario.GetRequestCount(url).Should().Be(2); - // } + [Theory] + [InlineData(CacheExpirationType.Absolute)] + [InlineData(CacheExpirationType.Sliding)] + public async Task GetUniversalItemAsync_TooManyItems_InvalidatedByItemsDependency(CacheExpirationType cacheExpirationType) + { + const string codename = "codename"; + var url = $"items/{codename}"; + var modularContent = Enumerable.Range(1, 51).Select(i => $"modular_{i}").Select(cn => (cn, CreateItem(cn))).ToList(); + var item = CreateItemResponse(CreateItem(codename, "original"), modularContent); + var updatedItem = CreateItemResponse(CreateItem(codename, "updated"), modularContent); + var scenarioBuilder = new ScenarioBuilder(cacheExpirationType: cacheExpirationType); + + var scenario = scenarioBuilder.WithResponse(url, item).Build(); + var firstResponse = await scenario.CachingClient.GetUniversalItemAsync(codename); + scenario = scenarioBuilder.WithResponse(url, updatedItem).Build(); + scenario.InvalidateDependency(CacheHelpers.GetItemsDependencyKey()); + var secondResponse = await scenario.CachingClient.GetUniversalItemAsync(codename); + //Check + firstResponse.Should().NotBeNull(); + secondResponse.Should().NotBeNull(); + firstResponse.Should().NotBeEquivalentTo(secondResponse); + scenario.GetRequestCount(url).Should().Be(2); + } // [Theory] // [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Absolute)] diff --git a/docs/retrieving-data/universal-item-retrieval.md b/docs/retrieving-data/universal-item-retrieval.md index 46132fc4..12f48b76 100644 --- a/docs/retrieving-data/universal-item-retrieval.md +++ b/docs/retrieving-data/universal-item-retrieval.md @@ -17,6 +17,7 @@ Mention `IPropertyValueConverter` and `IPropertyMapper` being excluded ## Caching * Mention retrieval key `IUniversalContentItem` + `IList` +* Mention max cache count (maybe somewhere else) `CacheHelpers.MAX_DEPENDENCY_ITEMS` > TODOS > From 39d94d09e2ecd148640546ea63787882c3352141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Fri, 31 Mar 2023 14:11:18 +0200 Subject: [PATCH 23/34] add test to combine both was of retrieving item with different cache key --- .../DeliveryClientCacheTests.cs | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs index 3d18f7a2..7fd2763d 100644 --- a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs +++ b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs @@ -870,7 +870,7 @@ public async Task GetUniversalItemAsync_TooManyItems_InvalidatedByItemsDependenc var item = CreateItemResponse(CreateItem(codename, "original"), modularContent); var updatedItem = CreateItemResponse(CreateItem(codename, "updated"), modularContent); var scenarioBuilder = new ScenarioBuilder(cacheExpirationType: cacheExpirationType); - + var scenario = scenarioBuilder.WithResponse(url, item).Build(); var firstResponse = await scenario.CachingClient.GetUniversalItemAsync(codename); scenario = scenarioBuilder.WithResponse(url, updatedItem).Build(); @@ -883,30 +883,30 @@ public async Task GetUniversalItemAsync_TooManyItems_InvalidatedByItemsDependenc scenario.GetRequestCount(url).Should().Be(2); } - // [Theory] - // [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Absolute)] - // [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Sliding)] - // [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Absolute)] - // [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Sliding)] - // public async Task GetItemTypedAsync_DifferentTypesAreCachedSeparately(CacheTypeEnum cacheType, CacheExpirationType cacheExpirationType) - // { - // const string codename = "codename"; - // var url = $"items/{codename}"; - // var item = CreateItemResponse(CreateItem(codename, "original")); - // var scenarioBuilder = new ScenarioBuilder(cacheType, cacheExpirationType); - // var scenario = scenarioBuilder.WithResponse(url, item).Build(); - // var firstResponse = await scenario.CachingClient.GetItemAsync(codename); - // var secondResponse = await scenario.CachingClient.GetItemAsync(codename); - // var repeatedFirstResponse = await scenario.CachingClient.GetItemAsync(codename); - // var repeatedSecondResponse = await scenario.CachingClient.GetItemAsync(codename); - // //Check - // firstResponse.Should().NotBeNull(); - // firstResponse.Should().BeEquivalentTo(repeatedFirstResponse); - // secondResponse.Should().NotBeNull(); - // secondResponse.Should().BeEquivalentTo(repeatedSecondResponse); - // firstResponse.Should().NotBeEquivalentTo(secondResponse); - // scenario.GetRequestCount(url).Should().Be(2); - // } + [Theory] + [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Absolute)] + [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Sliding)] + [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Absolute)] + [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Sliding)] + public async Task GetUniversalItemAsync_With_GetItemAsync_DifferentTypesAreCachedSeparately(CacheTypeEnum cacheType, CacheExpirationType cacheExpirationType) + { + const string codename = "codename"; + var url = $"items/{codename}"; + var item = CreateItemResponse(CreateItem(codename, "original")); + var scenarioBuilder = new ScenarioBuilder(cacheType, cacheExpirationType); + var scenario = scenarioBuilder.WithResponse(url, item).Build(); + var firstResponse = await scenario.CachingClient.GetUniversalItemAsync(codename); + var secondResponse = await scenario.CachingClient.GetItemAsync(codename); + var repeatedFirstResponse = await scenario.CachingClient.GetUniversalItemAsync(codename); + var repeatedSecondResponse = await scenario.CachingClient.GetItemAsync(codename); + //Check + firstResponse.Should().NotBeNull(); + firstResponse.Should().BeEquivalentTo(repeatedFirstResponse); + secondResponse.Should().NotBeNull(); + secondResponse.Should().BeEquivalentTo(repeatedSecondResponse); + firstResponse.Should().NotBeEquivalentTo(secondResponse); + scenario.GetRequestCount(url).Should().Be(2); + } #endregion From f421dc605fd8dca9be4901739f96116f9c37ceef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Tue, 4 Apr 2023 10:50:47 +0200 Subject: [PATCH 24/34] add listing tests (and a few TODOs) --- .../DeliveryClientCacheTests.cs | 69 ++++++++++++++++++- Kontent.Ai.Delivery.Caching/CacheHelpers.cs | 7 +- .../DeliveryClientCache.cs | 3 + 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs index 7fd2763d..f4940e1b 100644 --- a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs +++ b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FakeItEasy; @@ -935,7 +936,73 @@ public async Task GetUniversalItemsAsync_ResponseIsCached(CacheTypeEnum cacheTyp scenario.GetRequestCount(url).Should().Be(1); } - // TODO Add all test to mirror strongly type responses + + [Theory] + [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Absolute)] + [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Sliding)] + [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Absolute)] + [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Sliding)] + public async Task GetUniversalItemsAsync_Paged_ResponseIsCached(CacheTypeEnum cacheType, CacheExpirationType cacheExpirationType) + { + var url = "items"; + var pagination = new Pagination(2, 100, 10, 68, "https://testme"); + var itemB = CreateItem("b", "original"); + var items = CreatePagedItemsResponse(new[] { CreateItem("a", "original"), itemB }, null, pagination); + var scenarioBuilder = new ScenarioBuilder(cacheType, cacheExpirationType); + var scenario = scenarioBuilder.WithResponse(url, items).Build(); + + var firstResponse = await scenario.CachingClient.GetUniversalItemsAsync(); + //Check + firstResponse.Should().NotBeNull(); + scenario.GetRequestCount(url).Should().Be(1); + firstResponse.Pagination.Should().BeEquivalentTo(pagination); + } + + [Theory] + [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Absolute)] + [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Sliding)] + [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Absolute)] + [InlineData(CacheTypeEnum.Distributed, CacheExpirationType.Sliding)] + public async Task GetUniversalItemsAsync_InvalidatedByItemsKey(CacheTypeEnum cacheType, CacheExpirationType cacheExpirationType) + { + var url = "items"; + var itemB = CreateItem("b", "original"); + var items = CreateItemsResponse(new[] { CreateItem("a", "original"), itemB }); + var updatedItems = CreateItemsResponse(new[] { CreateItem("a", "updated"), itemB }); + var scenarioBuilder = new ScenarioBuilder(cacheType, cacheExpirationType); + var scenario = scenarioBuilder.WithResponse(url, items).Build(); + var firstResponse = await scenario.CachingClient.GetUniversalItemsAsync(); + scenario = scenarioBuilder.WithResponse(url, updatedItems).Build(); + scenario.InvalidateDependency(CacheHelpers.GetItemsKey>(Enumerable.Empty())); + var secondResponse = await scenario.CachingClient.GetUniversalItemsAsync(); + //Check + firstResponse.Should().NotBeNull(); + secondResponse.Should().NotBeNull(); + firstResponse.Should().NotBeEquivalentTo(secondResponse); + scenario.GetRequestCount(url).Should().Be(2); + } + + [Theory] + [InlineData(CacheExpirationType.Absolute)] + [InlineData(CacheExpirationType.Sliding)] + public async Task GetUniversalItemsAsync_InvalidatedByItemsDependency(CacheExpirationType cacheExpirationType) + { + var url = "items"; + var itemB = CreateItem("b", "original"); + var items = CreateItemsResponse(new[] { CreateItem("a", "original"), itemB }); + var updatedItems = CreateItemsResponse(new[] { CreateItem("a", "updated"), itemB }); + var scenarioBuilder = new ScenarioBuilder(cacheExpirationType: cacheExpirationType); + var scenario = scenarioBuilder.WithResponse(url, items).Build(); + var firstResponse = await scenario.CachingClient.GetUniversalItemsAsync(); + scenario = scenarioBuilder.WithResponse(url, updatedItems).Build(); + scenario.InvalidateDependency(CacheHelpers.GetItemsDependencyKey()); + var secondResponse = await scenario.CachingClient.GetUniversalItemsAsync(); + //Check + firstResponse.Should().NotBeNull(); + secondResponse.Should().NotBeNull(); + firstResponse.Should().NotBeEquivalentTo(secondResponse); + scenario.GetRequestCount(url).Should().Be(2); + } #endregion } diff --git a/Kontent.Ai.Delivery.Caching/CacheHelpers.cs b/Kontent.Ai.Delivery.Caching/CacheHelpers.cs index 3468da2d..a2b92dad 100644 --- a/Kontent.Ai.Delivery.Caching/CacheHelpers.cs +++ b/Kontent.Ai.Delivery.Caching/CacheHelpers.cs @@ -349,8 +349,11 @@ private static bool IsAssignableToGenericType(Type givenType, Type genericType) private static bool IsItemListingResponse(IResponse response) { - return response.GetType().IsGenericType && - IsAssignableToGenericType(response.GetType(), typeof(IDeliveryItemListingResponse<>)); + return + ( + response.GetType().IsGenericType + && IsAssignableToGenericType(response.GetType(), typeof(IDeliveryItemListingResponse<>)) + ) || typeof(IDeliveryUniversalItemListingResponse).IsAssignableFrom(response.GetType()); } private static bool IsComponent(JProperty property) diff --git a/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs b/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs index bafe51b8..54af06a6 100644 --- a/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs +++ b/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs @@ -160,6 +160,7 @@ public async Task GetLanguagesAsync(IEnumerabl // TODO public async Task GetUniversalItemAsync(string codename, IEnumerable parameters = null) { + // TODO use different method for getting item cache keys? var queryParameters = parameters?.ToList(); return await _deliveryCacheManager.GetOrAddAsync( CacheHelpers.GetItemKey(codename, queryParameters), @@ -171,6 +172,8 @@ public async Task GetUniversalItemAsync(string c // TODO public async Task GetUniversalItemsAsync(IEnumerable parameters = null) { + // TODO CacheHelpers.GetItemsDependencies - is that correct to use the same key? + // TODO CacheHelpers.GetItemsKey do we want to have it here? var queryParameters = parameters?.ToList(); return await _deliveryCacheManager.GetOrAddAsync( CacheHelpers.GetItemsKey>(queryParameters), From 89b6e5c70925289429d0031c5054a9a02b55d176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Tue, 4 Apr 2023 16:04:47 +0200 Subject: [PATCH 25/34] format --- Kontent.Ai.Delivery.Caching/CacheHelpers.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Kontent.Ai.Delivery.Caching/CacheHelpers.cs b/Kontent.Ai.Delivery.Caching/CacheHelpers.cs index a2b92dad..58b8b231 100644 --- a/Kontent.Ai.Delivery.Caching/CacheHelpers.cs +++ b/Kontent.Ai.Delivery.Caching/CacheHelpers.cs @@ -197,7 +197,7 @@ public static IEnumerable GetItemDependencies(IResponse response) { var dependencies = new HashSet(); - // TODO for univeral dependencies + // TODO for universal dependencies if (!IsItemResponse(response)) { return dependencies; @@ -225,7 +225,6 @@ public static IEnumerable GetItemDependencies(IResponse response) } } - return dependencies.Count > MAX_DEPENDENCY_ITEMS ? new[] { GetItemsDependencyKey() } : dependencies.AsEnumerable(); From 000e1688c050627044d33d40a473b9fe655cc085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Wed, 5 Apr 2023 20:26:19 +0200 Subject: [PATCH 26/34] get rid of TODOs vol.1 --- .../ContentItems/Elements/ContentElementValue.cs | 10 +--------- .../ContentItems/Universal/GenericModelProvider.cs | 1 + .../Extensions/IContentElementValueExtensions.cs | 13 +++++++++++++ docs/retrieving-data/caching.md | 3 +-- docs/retrieving-data/universal-item-retrieval.md | 5 +++++ 5 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 Kontent.Ai.Delivery/Extensions/IContentElementValueExtensions.cs diff --git a/Kontent.Ai.Delivery/ContentItems/Elements/ContentElementValue.cs b/Kontent.Ai.Delivery/ContentItems/Elements/ContentElementValue.cs index 7731114b..4c2db5ce 100644 --- a/Kontent.Ai.Delivery/ContentItems/Elements/ContentElementValue.cs +++ b/Kontent.Ai.Delivery/ContentItems/Elements/ContentElementValue.cs @@ -3,7 +3,7 @@ namespace Kontent.Ai.Delivery.ContentItems.Elements { - internal class ContentElementValue : IContentElementValue + internal abstract class ContentElementValue : IContentElementValue { /// [JsonProperty("type")] @@ -20,13 +20,5 @@ internal class ContentElementValue : IContentElementValue /// [JsonProperty("codename")] public string Codename { get; internal set; } - - // TODO is that a good idea? - public ContentElementValue WithCodename(string codename) - { - Codename = codename; - return this; - } - } } diff --git a/Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs index a940fd9e..e9fc6855 100644 --- a/Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Kontent.Ai.Delivery.Abstractions; using Kontent.Ai.Delivery.ContentItems.Elements; +using Kontent.Ai.Delivery.Extensions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/Kontent.Ai.Delivery/Extensions/IContentElementValueExtensions.cs b/Kontent.Ai.Delivery/Extensions/IContentElementValueExtensions.cs new file mode 100644 index 00000000..aaf622b0 --- /dev/null +++ b/Kontent.Ai.Delivery/Extensions/IContentElementValueExtensions.cs @@ -0,0 +1,13 @@ +using Kontent.Ai.Delivery.ContentItems.Elements; + +namespace Kontent.Ai.Delivery.Extensions +{ + internal static class IContentElementValueExtensions + { + internal static ContentElementValue WithCodename(this ContentElementValue elementValue, string codename) + { + elementValue.Codename = codename; + return elementValue; + } + } +} \ No newline at end of file diff --git a/docs/retrieving-data/caching.md b/docs/retrieving-data/caching.md index 9342f337..3536bb81 100644 --- a/docs/retrieving-data/caching.md +++ b/docs/retrieving-data/caching.md @@ -97,7 +97,6 @@ So for cache eviction, one can only use like keys generated by `CacheHelpers.Get Please not that it's also not possible to wipe the whole cache (which is, in a way, [possible](https://stackoverflow.com/a/45543023/1332034) with the `IMemoryCache`) and therefore, for cache invalidation, one can only rely on [absolute or sliding expiration](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.distributed.distributedcacheentryoptions?view=dotnet-plat-ext-3.1) if the cached keys. - ## Customizing caching -You can also provide your own version of the caching mechanism by implementing the [`IDeliveryCacheManager`](https://github.com/kontent-ai/delivery-sdk-net/Kontent.Ai.Delivery.Abstractions/IDeliveryCacheManager.cs) interface. \ No newline at end of file +You can also provide your own version of the caching mechanism by implementing the [`IDeliveryCacheManager`](https://github.com/kontent-ai/delivery-sdk-net/Kontent.Ai.Delivery.Abstractions/IDeliveryCacheManager.cs) interface. diff --git a/docs/retrieving-data/universal-item-retrieval.md b/docs/retrieving-data/universal-item-retrieval.md index 12f48b76..970462fb 100644 --- a/docs/retrieving-data/universal-item-retrieval.md +++ b/docs/retrieving-data/universal-item-retrieval.md @@ -4,8 +4,13 @@ Intro ## Retrieve data +* use sync vs. async method for IUniversalModelProvider? + ## Data structure +* mention why not record classes +* withCodename method? + ## Convert to strongly type models Explain, or raise issue From 5a119402087466e17e08e8862ac097bfb1fab523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Wed, 5 Apr 2023 20:26:36 +0200 Subject: [PATCH 27/34] get rid of TODOs vol.2 --- .../IDeliveryUniversalItemListingResponse.cs | 1 - .../Universal/IUniversalContentItem.cs | 1 - .../IDeliveryClient.cs | 1 - .../DeliveryClientCacheTests.cs | 2 -- Kontent.Ai.Delivery.Caching/CacheHelpers.cs | 1 - .../DeliveryClientCache.cs | 1 - .../Factories/DeliveryClientFactory.cs | 5 +++-- .../DeliveryClientBuilderImplementation.cs | 3 +++ .../DeliveryClient/IDeliveryClientBuilder.cs | 7 +++++++ ...Provider.cs => UniversalItemModelProvider.cs} | 10 +++++----- Kontent.Ai.Delivery/DeliveryClient.cs | 16 ++++++++-------- .../Extensions/ServiceCollectionExtensions.cs | 2 ++ docs/retrieving-data/universal-item-retrieval.md | 9 ++++++++- 13 files changed, 36 insertions(+), 23 deletions(-) rename Kontent.Ai.Delivery/ContentItems/Universal/{GenericModelProvider.cs => UniversalItemModelProvider.cs} (88%) diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IDeliveryUniversalItemListingResponse.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IDeliveryUniversalItemListingResponse.cs index b7a6b505..83cc8433 100644 --- a/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IDeliveryUniversalItemListingResponse.cs +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IDeliveryUniversalItemListingResponse.cs @@ -7,7 +7,6 @@ public interface IDeliveryUniversalItemListingResponse : IResponse, IPageable /// /// Gets the content item. /// - // TODO why it is IList IList Items { get; } diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalContentItem.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalContentItem.cs index d42af0a4..dcb654b4 100644 --- a/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalContentItem.cs +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalContentItem.cs @@ -2,7 +2,6 @@ namespace Kontent.Ai.Delivery.Abstractions { - // TODO validate this to get whole element context /// /// Dynamic representation of the content item dynamic item processing. /// Values are based on diff --git a/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs b/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs index bea1ce05..de5cff09 100644 --- a/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs +++ b/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs @@ -29,7 +29,6 @@ public interface IDeliveryClient /// The instance that contains the content items. If no query parameters are specified, all content items are returned. Task> GetItemsAsync(IEnumerable parameters = null); - // TODO extension methods Task GetUniversalItemsAsync(IEnumerable parameters = null); /// diff --git a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs index f4940e1b..5422ff7f 100644 --- a/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs +++ b/Kontent.Ai.Delivery.Caching.Tests/DeliveryClientCacheTests.cs @@ -760,8 +760,6 @@ public async Task GetUniversalItemAsync_ResponseIsCached(CacheTypeEnum cacheType scenario.GetRequestCount(url).Should().Be(1); } - // TODO Add all test to mirror strongly type responses - [Theory] [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Absolute)] [InlineData(CacheTypeEnum.Memory, CacheExpirationType.Sliding)] diff --git a/Kontent.Ai.Delivery.Caching/CacheHelpers.cs b/Kontent.Ai.Delivery.Caching/CacheHelpers.cs index 58b8b231..8a1cfff9 100644 --- a/Kontent.Ai.Delivery.Caching/CacheHelpers.cs +++ b/Kontent.Ai.Delivery.Caching/CacheHelpers.cs @@ -197,7 +197,6 @@ public static IEnumerable GetItemDependencies(IResponse response) { var dependencies = new HashSet(); - // TODO for universal dependencies if (!IsItemResponse(response)) { return dependencies; diff --git a/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs b/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs index 54af06a6..d95353ea 100644 --- a/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs +++ b/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs @@ -160,7 +160,6 @@ public async Task GetLanguagesAsync(IEnumerabl // TODO public async Task GetUniversalItemAsync(string codename, IEnumerable parameters = null) { - // TODO use different method for getting item cache keys? var queryParameters = parameters?.ToList(); return await _deliveryCacheManager.GetOrAddAsync( CacheHelpers.GetItemKey(codename, queryParameters), diff --git a/Kontent.Ai.Delivery.Tests/Factories/DeliveryClientFactory.cs b/Kontent.Ai.Delivery.Tests/Factories/DeliveryClientFactory.cs index ea916f22..2de12c44 100644 --- a/Kontent.Ai.Delivery.Tests/Factories/DeliveryClientFactory.cs +++ b/Kontent.Ai.Delivery.Tests/Factories/DeliveryClientFactory.cs @@ -2,6 +2,7 @@ using System.Net.Http; using FakeItEasy; using Kontent.Ai.Delivery.Abstractions; +using Kontent.Ai.Delivery.ContentItems.Universal; using RichardSzalay.MockHttp; namespace Kontent.Ai.Delivery.Tests.Factories @@ -17,7 +18,7 @@ internal static DeliveryClient GetMockedDeliveryClientWithProjectId( IModelProvider modelProvider = null, IRetryPolicyProvider resiliencePolicyProvider = null, ITypeProvider typeProvider = null, - IUniversalItemModelProvider genericModelProvider = null) + IUniversalItemModelProvider universalItemModelProvider = null) { var httpClient = GetHttpClient(httpMessageHandler); @@ -29,7 +30,7 @@ internal static DeliveryClient GetMockedDeliveryClientWithProjectId( new DeliveryHttpClient(httpClient), Serializer, null, - genericModelProvider ?? A.Fake() + universalItemModelProvider ?? new UniversalItemModelProvider(Serializer) ); return client; diff --git a/Kontent.Ai.Delivery/Builders/DeliveryClient/DeliveryClientBuilderImplementation.cs b/Kontent.Ai.Delivery/Builders/DeliveryClient/DeliveryClientBuilderImplementation.cs index 7c579cb7..9edc4422 100644 --- a/Kontent.Ai.Delivery/Builders/DeliveryClient/DeliveryClientBuilderImplementation.cs +++ b/Kontent.Ai.Delivery/Builders/DeliveryClient/DeliveryClientBuilderImplementation.cs @@ -61,6 +61,9 @@ IOptionalClientSetup IOptionalClientSetup.WithPropertyMapper(IPropertyMapper pro IOptionalClientSetup IOptionalClientSetup.WithLoggerFactory(ILoggerFactory loggerFactory) => RegisterOrThrow(loggerFactory, nameof(loggerFactory)); + IOptionalClientSetup IOptionalClientSetup.WithUniversalItemModelProvider(IUniversalItemModelProvider universalItemModelProvider) + => RegisterOrThrow(universalItemModelProvider, nameof(universalItemModelProvider)); + IDeliveryClient IDeliveryClientBuild.Build() { _serviceCollection.AddDeliveryClient(_deliveryOptions); diff --git a/Kontent.Ai.Delivery/Builders/DeliveryClient/IDeliveryClientBuilder.cs b/Kontent.Ai.Delivery/Builders/DeliveryClient/IDeliveryClientBuilder.cs index 1405f2d6..a771cc45 100644 --- a/Kontent.Ai.Delivery/Builders/DeliveryClient/IDeliveryClientBuilder.cs +++ b/Kontent.Ai.Delivery/Builders/DeliveryClient/IDeliveryClientBuilder.cs @@ -80,6 +80,13 @@ public interface IOptionalClientSetup : IDeliveryClientBuild /// /// IOptionalClientSetup WithLoggerFactory(ILoggerFactory loggerFactory); + + /// + /// Use custom IUniversalItemModelProvider to map elements to thy . + /// + /// + /// + IOptionalClientSetup WithUniversalItemModelProvider(IUniversalItemModelProvider universalItemModelProvider); } /// diff --git a/Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/Universal/UniversalItemModelProvider.cs similarity index 88% rename from Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs rename to Kontent.Ai.Delivery/ContentItems/Universal/UniversalItemModelProvider.cs index e9fc6855..4fe517f9 100644 --- a/Kontent.Ai.Delivery/ContentItems/Universal/GenericModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/Universal/UniversalItemModelProvider.cs @@ -9,11 +9,11 @@ namespace Kontent.Ai.Delivery.ContentItems.Universal { - internal class GenericModelProvider : IUniversalItemModelProvider + internal class UniversalItemModelProvider : IUniversalItemModelProvider { internal readonly JsonSerializer Serializer; - internal GenericModelProvider(JsonSerializer serializer) + public UniversalItemModelProvider(JsonSerializer serializer) { Serializer = serializer; } @@ -21,9 +21,9 @@ internal GenericModelProvider(JsonSerializer serializer) public async Task GetContentItemGenericModelAsync(object item) => (IUniversalContentItem)await GetContentItemModelAsync((JObject)item); - internal async Task GetContentItemModelAsync(JObject serializedItem) + internal Task GetContentItemModelAsync(JObject serializedItem) { - var result = new UniversalContentItem() { + IUniversalContentItem result = new UniversalContentItem() { System = serializedItem?["system"]?.ToObject(Serializer) }; @@ -54,7 +54,7 @@ internal async Task GetContentItemModelAsync(JObject seri result.Elements.Add(key, value); } - return result; + return Task.FromResult(result); } } } \ No newline at end of file diff --git a/Kontent.Ai.Delivery/DeliveryClient.cs b/Kontent.Ai.Delivery/DeliveryClient.cs index 224829fa..690ce266 100644 --- a/Kontent.Ai.Delivery/DeliveryClient.cs +++ b/Kontent.Ai.Delivery/DeliveryClient.cs @@ -38,7 +38,7 @@ internal sealed class DeliveryClient : IDeliveryClient internal readonly IDeliveryHttpClient DeliveryHttpClient; internal readonly JsonSerializer Serializer; internal readonly ILoggerFactory LoggerFactory; - internal readonly IUniversalItemModelProvider GenericModelProvider; + internal readonly IUniversalItemModelProvider UniversalItemModelProvider; internal DeliveryEndpointUrlBuilder UrlBuilder => _urlBuilder ??= new DeliveryEndpointUrlBuilder(DeliveryOptions); @@ -53,6 +53,7 @@ internal DeliveryEndpointUrlBuilder UrlBuilder /// An instance of an object that can send request against Kontent.ai Delivery API /// Default JSON serializer /// The factory used to create loggers + /// An instance of an object that can JSON responses into the public DeliveryClient( IOptionsMonitor deliveryOptions, IModelProvider modelProvider = null, @@ -62,7 +63,7 @@ public DeliveryClient( JsonSerializer serializer = null, // TODO why logger factory is not everywhere ? ILoggerFactory loggerFactory = null, - IUniversalItemModelProvider genericModelProvider = null) + IUniversalItemModelProvider universalItemModelProvider = null) { DeliveryOptions = deliveryOptions; ModelProvider = modelProvider; @@ -71,8 +72,7 @@ public DeliveryClient( DeliveryHttpClient = deliveryHttpClient; Serializer = serializer; LoggerFactory = loggerFactory; - // TODO IOC? Default? Check references - GenericModelProvider = new GenericModelProvider(serializer); + UniversalItemModelProvider = universalItemModelProvider; } /// @@ -336,14 +336,14 @@ public async Task GetUniversalItemAsync(string c } var content = await response.GetJsonContentAsync(); - var model = await GenericModelProvider.GetContentItemGenericModelAsync(content["item"]); + var model = await UniversalItemModelProvider.GetContentItemGenericModelAsync(content["item"]); var linkedUniversalItems = await Task.WhenAll( content["modular_content"]? .Values() .Select(async linkedItem => { - var model = await GenericModelProvider.GetContentItemGenericModelAsync(linkedItem); + var model = await UniversalItemModelProvider.GetContentItemGenericModelAsync(linkedItem); return new KeyValuePair(model.System.Codename, model); }) ); @@ -367,14 +367,14 @@ public async Task GetUniversalItemsAsync( var content = await response.GetJsonContentAsync(); var pagination = content["pagination"].ToObject(Serializer); - var items = ((JArray)content["items"]).Select(async source => await GenericModelProvider.GetContentItemGenericModelAsync(source)); + var items = ((JArray)content["items"]).Select(async source => await UniversalItemModelProvider.GetContentItemGenericModelAsync(source)); var linkedUniversalItems = await Task.WhenAll( content["modular_content"]? .Values() .Select(async linkedItem => { - var model = await GenericModelProvider.GetContentItemGenericModelAsync(linkedItem); + var model = await UniversalItemModelProvider.GetContentItemGenericModelAsync(linkedItem); return new KeyValuePair(model.System.Codename, model); }) ); diff --git a/Kontent.Ai.Delivery/Extensions/ServiceCollectionExtensions.cs b/Kontent.Ai.Delivery/Extensions/ServiceCollectionExtensions.cs index d054d34d..d58c4789 100644 --- a/Kontent.Ai.Delivery/Extensions/ServiceCollectionExtensions.cs +++ b/Kontent.Ai.Delivery/Extensions/ServiceCollectionExtensions.cs @@ -6,6 +6,7 @@ using Kontent.Ai.Delivery.ContentItems; using Kontent.Ai.Delivery.ContentItems.ContentLinks; using Kontent.Ai.Delivery.ContentItems.InlineContentItems; +using Kontent.Ai.Delivery.ContentItems.Universal; using Kontent.Ai.Delivery.Helpers; using Kontent.Ai.Delivery.RetryPolicy; using Microsoft.Extensions.Configuration; @@ -121,6 +122,7 @@ public static IServiceCollection RegisterDependencies(this IServiceCollection se services.TryAddSingleton(new DeliveryJsonSerializer()); services.TryAddSingleton(); services.TryAddSingleton(NullLoggerFactory.Instance); + services.TryAddSingleton(); return services; } diff --git a/docs/retrieving-data/universal-item-retrieval.md b/docs/retrieving-data/universal-item-retrieval.md index 970462fb..a11f5060 100644 --- a/docs/retrieving-data/universal-item-retrieval.md +++ b/docs/retrieving-data/universal-item-retrieval.md @@ -2,6 +2,8 @@ Intro + + ## Retrieve data * use sync vs. async method for IUniversalModelProvider? @@ -9,7 +11,8 @@ Intro ## Data structure * mention why not record classes -* withCodename method? +* withCodename method/extension +* do we want to use IList/IEnumerable for modular content? ## Convert to strongly type models @@ -19,6 +22,10 @@ Explain, or raise issue Mention `IPropertyValueConverter` and `IPropertyMapper` being excluded +## Customization + +* Mention DeliveryCLientFactory for registering custom + ## Caching * Mention retrieval key `IUniversalContentItem` + `IList` From 6c4c671a690031408d90adcd6841029f0b2ba541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Wed, 5 Apr 2023 20:27:48 +0200 Subject: [PATCH 28/34] get rid of TODOs vol.3 --- .../ContentItems/Elements/IContentElementValue.cs | 1 - .../ContentItems/Universal/UniversalItemModelProvider.cs | 5 ----- Kontent.Ai.Delivery/DeliveryClient.cs | 1 - 3 files changed, 7 deletions(-) diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IContentElementValue.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IContentElementValue.cs index e797948c..18a0e2b6 100644 --- a/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IContentElementValue.cs +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Elements/IContentElementValue.cs @@ -1,6 +1,5 @@ namespace Kontent.Ai.Delivery.Abstractions { - // TODO think whether the IContentElementValue is needed /// /// Represents a generic content element with value. /// diff --git a/Kontent.Ai.Delivery/ContentItems/Universal/UniversalItemModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/Universal/UniversalItemModelProvider.cs index 4fe517f9..97f89c26 100644 --- a/Kontent.Ai.Delivery/ContentItems/Universal/UniversalItemModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/Universal/UniversalItemModelProvider.cs @@ -32,19 +32,14 @@ internal Task GetContentItemModelAsync(JObject serialized var key = item.Key; var element = item.Value; - // TODO think about value converter implementation - // TODO what about codename property - now it is not null, bit withCodename is not really nice IContentElementValue value = (element["type"].ToString()) switch { - // TODO do we want to use string/structured data for rich text - probably think about support both ways "rich_text" => element.ToObject(Serializer).WithCodename(key), "asset" => element.ToObject(Serializer).WithCodename(key), "number" => element.ToObject(Serializer).WithCodename(key), - // TODO do we want to use string/structured data for date time => structured is OK "date_time" => element.ToObject(Serializer).WithCodename(key), "multiple_choice" => element.ToObject(Serializer).WithCodename(key), "taxonomy" => element.ToObject(Serializer).WithCodename(key), - // TODO what Linked items + what SubPages? "modular_content" => element.ToObject(Serializer).WithCodename(key), "custom" => element.ToObject(Serializer).WithCodename(key), "url_slug" => element.ToObject(Serializer).WithCodename(key), diff --git a/Kontent.Ai.Delivery/DeliveryClient.cs b/Kontent.Ai.Delivery/DeliveryClient.cs index 690ce266..57b99df1 100644 --- a/Kontent.Ai.Delivery/DeliveryClient.cs +++ b/Kontent.Ai.Delivery/DeliveryClient.cs @@ -61,7 +61,6 @@ public DeliveryClient( ITypeProvider typeProvider = null, IDeliveryHttpClient deliveryHttpClient = null, JsonSerializer serializer = null, - // TODO why logger factory is not everywhere ? ILoggerFactory loggerFactory = null, IUniversalItemModelProvider universalItemModelProvider = null) { From e8aeddd8605b27b82765acab73adf875dd4fd04b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Wed, 5 Apr 2023 20:35:23 +0200 Subject: [PATCH 29/34] get rid of TODOs vol.4 --- .../Universal/IUniversalContentItem.cs | 4 +++- .../IDeliveryClient.cs | 12 +++++++++++- .../DeliveryClientCache.cs | 15 +++++++++++---- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalContentItem.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalContentItem.cs index dcb654b4..456bc100 100644 --- a/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalContentItem.cs +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalContentItem.cs @@ -8,7 +8,9 @@ namespace Kontent.Ai.Delivery.Abstractions /// public interface IUniversalContentItem : IContentItem { - // TODO + /// + /// Represents content items elements. + /// public Dictionary Elements { get; set; } } } diff --git a/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs b/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs index de5cff09..e425e03e 100644 --- a/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs +++ b/Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs @@ -18,7 +18,12 @@ public interface IDeliveryClient Task> GetItemAsync(string codename, IEnumerable parameters = null); - // TODO + /// + /// Returns item and its linked items in the form. + /// + /// The codename of a content item. + /// A collection of query parameters, for example for projection or depth of linked items. + /// The instance that contains the content item and linked items with the specified codename. Task GetUniversalItemAsync(string codename, IEnumerable parameters = null); /// @@ -29,6 +34,11 @@ public interface IDeliveryClient /// The instance that contains the content items. If no query parameters are specified, all content items are returned. Task> GetItemsAsync(IEnumerable parameters = null); + /// + /// Returns content items that match the optional filtering parameters. By default, retrieves one level of linked items. + /// + /// A collection of query parameters, for example, for filtering, ordering, or setting the depth of linked items. + /// The instance that contains the content items. If no query parameters are specified, all content items in the first page are returned. Task GetUniversalItemsAsync(IEnumerable parameters = null); /// diff --git a/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs b/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs index d95353ea..9e773008 100644 --- a/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs +++ b/Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs @@ -157,7 +157,12 @@ public async Task GetLanguagesAsync(IEnumerabl CacheHelpers.GetLanguagesDependencies); } - // TODO + /// + /// Returns item and its linked items in the form. + /// + /// The codename of a content item. + /// A collection of query parameters, for example for projection or depth of linked items. + /// The instance that contains the content item and linked items with the specified codename. public async Task GetUniversalItemAsync(string codename, IEnumerable parameters = null) { var queryParameters = parameters?.ToList(); @@ -168,11 +173,13 @@ public async Task GetUniversalItemAsync(string c CacheHelpers.GetItemDependencies); } - // TODO + /// + /// Returns content items that match the optional filtering parameters. By default, retrieves one level of linked items. + /// + /// A collection of query parameters, for example, for filtering, ordering, or setting the depth of linked items. + /// The instance that contains the content items. If no query parameters are specified, all content items in the first page are returned. public async Task GetUniversalItemsAsync(IEnumerable parameters = null) { - // TODO CacheHelpers.GetItemsDependencies - is that correct to use the same key? - // TODO CacheHelpers.GetItemsKey do we want to have it here? var queryParameters = parameters?.ToList(); return await _deliveryCacheManager.GetOrAddAsync( CacheHelpers.GetItemsKey>(queryParameters), From 602a4069f40e81e82e84170c5a9f5333825fc8d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Wed, 5 Apr 2023 21:35:07 +0200 Subject: [PATCH 30/34] cover deliveryClientBuilder with universalitemmodelprovider registration tests --- Kontent.Ai.Delivery.Caching.Tests/Scenario.cs | 5 ++++- .../DeliveryClient/DeliveryClientBuilderTests.cs | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Kontent.Ai.Delivery.Caching.Tests/Scenario.cs b/Kontent.Ai.Delivery.Caching.Tests/Scenario.cs index 5ef844a6..ba094eab 100644 --- a/Kontent.Ai.Delivery.Caching.Tests/Scenario.cs +++ b/Kontent.Ai.Delivery.Caching.Tests/Scenario.cs @@ -19,7 +19,10 @@ public Scenario(IMemoryCache memoryCache, CacheExpirationType cacheExpirationTyp { _requestCounter = requestCounter; _cacheManager = new MemoryCacheManager(memoryCache, Options.Create(new DeliveryCacheOptions { DefaultExpirationType = cacheExpirationType })); - var baseClient = DeliveryClientBuilder.WithOptions(_ => deliveryOptions).WithDeliveryHttpClient(new DeliveryHttpClient(httpClient)).Build(); + var baseClient = DeliveryClientBuilder + .WithOptions(_ => deliveryOptions) + .WithDeliveryHttpClient(new DeliveryHttpClient(httpClient)) + .Build(); CachingClient = new DeliveryClientCache(_cacheManager, baseClient); } diff --git a/Kontent.Ai.Delivery.Tests/Builders/DeliveryClient/DeliveryClientBuilderTests.cs b/Kontent.Ai.Delivery.Tests/Builders/DeliveryClient/DeliveryClientBuilderTests.cs index db8225a8..c0a074c6 100644 --- a/Kontent.Ai.Delivery.Tests/Builders/DeliveryClient/DeliveryClientBuilderTests.cs +++ b/Kontent.Ai.Delivery.Tests/Builders/DeliveryClient/DeliveryClientBuilderTests.cs @@ -64,6 +64,7 @@ public void BuildWithOptionalSteps_ReturnsDeliveryClientWithSetInstances() var mockTypeProvider = A.Fake(); var mockDeliveryHttpClient = new DeliveryHttpClient(new MockHttpMessageHandler().ToHttpClient()); var mockLoggerFactory = A.Fake(); + var mockUniversalItemModelProvider = A.Fake(); var deliveryClient = (Delivery.DeliveryClient) DeliveryClientBuilder .WithProjectId(ProjectId) @@ -78,6 +79,7 @@ public void BuildWithOptionalSteps_ReturnsDeliveryClientWithSetInstances() .WithRetryPolicyProvider(mockRetryPolicyProvider) .WithTypeProvider(mockTypeProvider) .WithLoggerFactory(mockLoggerFactory) + .WithUniversalItemModelProvider(mockUniversalItemModelProvider) .Build(); Assert.Equal(ProjectId, deliveryClient.DeliveryOptions.CurrentValue.ProjectId); @@ -86,6 +88,7 @@ public void BuildWithOptionalSteps_ReturnsDeliveryClientWithSetInstances() Assert.Equal(mockTypeProvider, deliveryClient.TypeProvider); Assert.Equal(mockDeliveryHttpClient, deliveryClient.DeliveryHttpClient); Assert.Equal(mockLoggerFactory, deliveryClient.LoggerFactory); + Assert.Equal(mockUniversalItemModelProvider, deliveryClient.UniversalItemModelProvider); } [Fact] @@ -222,6 +225,14 @@ public void BuildWithOptionsAndNullLoggerFactory_TrowsArgumentNullException() Assert.Throws(() => builderStep.WithLoggerFactory(null)); } + [Fact] + public void BuildWithOptionsAndNullUniversalItemModelProvider_TrowsArgumentNullException() + { + var builderStep = DeliveryClientBuilder.WithProjectId(_guid); + + Assert.Throws(() => builderStep.WithUniversalItemModelProvider(null)); + } + private static IEnumerable GetResolvableInlineContentItemTypes(Delivery.DeliveryClient deliveryClient) => (((ModelProvider)deliveryClient.ModelProvider).InlineContentItemsProcessor as InlineContentItemsProcessor).ContentItemResolvers.Keys; } From afa8b97b1e0eb88449e5dd237712bd041f59f236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Wed, 5 Apr 2023 21:35:17 +0200 Subject: [PATCH 31/34] prepare documentation --- .../strongly-typed-models.md | 7 +- .../universal-item-retrieval.md | 91 +++++++++++++++---- 2 files changed, 79 insertions(+), 19 deletions(-) diff --git a/docs/customization-and-extensibility/strongly-typed-models.md b/docs/customization-and-extensibility/strongly-typed-models.md index 567409f3..815961de 100644 --- a/docs/customization-and-extensibility/strongly-typed-models.md +++ b/docs/customization-and-extensibility/strongly-typed-models.md @@ -9,6 +9,7 @@ - [Typing simple elements](#typing-simple-elements) - [Typing Linked Items](#typing-linked-items) - [Typing Rich text](#typing-rich-text) + - [Typing Date and time](#typing-date-and-time) - [Typing Custom elements](#typing-custom-elements) - [Naming the properties](#naming-the-properties) - [Examples](#examples) @@ -23,6 +24,7 @@ ## Strongly-typed models + The `IDeliveryClient` interface supports fetching of strongly-typed models. ```csharp @@ -31,9 +33,6 @@ IDeliveryClient deliveryClient = DeliveryClientBuilder .WithProjectId("975bf280-fd91-488c-994c-2f04416e5ee3") .Build(); -// Basic retrieval -deliveryClient.GetItemAsync("article_about_coffee"); - // Strongly-typed model retrieval deliveryClient.GetItemAsync
("article_about_coffee"); ``` @@ -44,6 +43,8 @@ This approach is beneficial for its: - convenience of usage by a developer (`@Article.ArticleTitle` vs. `@Article.GetString("article_title")`) - support of type-dependent functionalities (such as [display templates](http://www.growingwiththeweb.com/2012/12/aspnet-mvc-display-and-editor-templates.html) in MVC) +> 💡 If you want to retrieve content items non-bound to the strongly typed models, you can try [universal item retrieval](../retrieving-data/universal-item-retrieval.md). + ## Defining a model The models are simple [POCO](https://en.wikipedia.org/wiki/Plain_old_CLR_object) classes, which means they don't have any attached behavior or dependency on an external framework. diff --git a/docs/retrieving-data/universal-item-retrieval.md b/docs/retrieving-data/universal-item-retrieval.md index a11f5060..e9479fe6 100644 --- a/docs/retrieving-data/universal-item-retrieval.md +++ b/docs/retrieving-data/universal-item-retrieval.md @@ -1,37 +1,96 @@ # Universal item retrieval -Intro +> ⚠ This approach is experimental. It might contain breaking changes in the future. +The `IDeliveryClient` interface supports fetching of universal content items. Which is basically content item(s) with elements being deserialized into the dictionary of elements and its linked item. +> See [Strongly model retrieval approach](../customization-and-extensibility/strongly-typed-models.md) for more strictly typed model retrieval. -## Retrieve data +```csharp +// Initializes a client +IDeliveryClient deliveryClient = DeliveryClientBuilder + .WithProjectId("975bf280-fd91-488c-994c-2f04416e5ee3") + .Build(); -* use sync vs. async method for IUniversalModelProvider? +// Basic retrieval +deliveryClient.GetUniversalItemAsync("article_about_coffee"); + +deliveryClient.GetUniversalItemsAsync(); +``` + +This approach is beneficial if you are processing items and do not distinguish among their content types i.e. spell check, procession based only pn element level. ## Data structure -* mention why not record classes -* withCodename method/extension -* do we want to use IList/IEnumerable for modular content? +The structure is simple. It mirror the [content item object](https://kontent.ai/learn/reference/openapi/delivery-api/#section/Content-item-object). The only thing that differs that the codename of elements is used as a key to dictionarry as well as property inside the value of the element. + +```csharp + +// Simplified +interface IUniversalContentItem +{ + IContentItemSystemAttributes System; + Dictionary Elements; +} + +} + +// Simplified +interface IContentElementValue : IContentElementValue +{ -## Convert to strongly type models + string Codename; + string Name; + string Type; + T Value; + // some element type specific properties +} +``` -Explain, or raise issue +The response content the item(s) depending on the ednpoint used. + +```csharp +// Simplified +interface IDeliveryUniversalItemResponse +{ + IUniversalContentItem Item; + public Dictionary LinkedItems; +} + +// Simplified +interface IDeliveryUniversalItemListingResponse +{ + IList Items; + Dictionary LinkedItems; +} +``` + +> It is possible to use `ModelProvider` to transform the `APIResponse` body to strongly typed model using the same approach as for [Typing modular content in response](../customization-and-extensibility/modular-content-in-response.md), but also for the item itself ## Exceptions -Mention `IPropertyValueConverter` and `IPropertyMapper` being excluded +Using [value converters](../customization-and-extensibility/value-converters.md) and [property mapper](../customization-and-extensibility/strongly-typed-models.md#customizing-the-property-matching) is not supported fot universal items. ## Customization -* Mention DeliveryCLientFactory for registering custom +You can implement your own `IUniversalModelProvider` and register it via `DeliveryClientFactory`. + +```csharp + +class CustomUniversalItemModelProvider : IUniversalItemModelProvider +{ + // custom implementation + public async Task GetContentItemGenericModelAsync(object item) => CustomImplementation((JObject)item); +} + +var client = DeliveryClientBuilder + .withProjectId("") + .WithUniversalItemModelProvider(new CustomUniversalItemModelProvider()) + .Build(); +``` ## Caching -* Mention retrieval key `IUniversalContentItem` + `IList` -* Mention max cache count (maybe somewhere else) `CacheHelpers.MAX_DEPENDENCY_ITEMS` +Univseral item retrieval is also supported in [caching client](../retrieving-data/caching.md). -> TODOS -> -> * [] Link from the root and in-between pages -> * [] Finish the docs \ No newline at end of file +There is the same caching implementation as for strongly typed methods `GetContentItem` and `GetContentItem`. Just as as the primary cache key `IUniversalContentItem` is used for type specification when retrieving single item and `IList` for retrieving multiple items. From 5d95e7d46e39fd168c1138b6356095a787addf41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= <9218736+Simply007@users.noreply.github.com> Date: Tue, 11 Apr 2023 14:06:48 +0200 Subject: [PATCH 32/34] Update universal-item-retrieval.md --- docs/retrieving-data/universal-item-retrieval.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/retrieving-data/universal-item-retrieval.md b/docs/retrieving-data/universal-item-retrieval.md index e9479fe6..2b2e76a6 100644 --- a/docs/retrieving-data/universal-item-retrieval.md +++ b/docs/retrieving-data/universal-item-retrieval.md @@ -2,7 +2,7 @@ > ⚠ This approach is experimental. It might contain breaking changes in the future. -The `IDeliveryClient` interface supports fetching of universal content items. Which is basically content item(s) with elements being deserialized into the dictionary of elements and its linked item. +The `IDeliveryClient` interface supports fetching the universal content items. Which is basically a form of representing content elements being deserialized into the dictionary of elements. And this universal item is retrieved as a part of universal response with its linked items. > See [Strongly model retrieval approach](../customization-and-extensibility/strongly-typed-models.md) for more strictly typed model retrieval. From 5787f665e7a13450eb528b2de5b0f5ae05979932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= Date: Thu, 13 Apr 2023 16:20:17 +0200 Subject: [PATCH 33/34] adjust the method name --- .../ContentItems/Universal/IUniversalModelProvider.cs | 10 +++++++++- .../Universal/UniversalItemModelProvider.cs | 2 +- Kontent.Ai.Delivery/DeliveryClient.cs | 8 ++++---- docs/retrieving-data/universal-item-retrieval.md | 2 +- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs index 0afd7660..d904e7f3 100644 --- a/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs @@ -5,8 +5,16 @@ namespace Kontent.Ai.Delivery.Abstractions { + /// + /// Interface ensuring mapping dynamic response to . + /// public interface IUniversalItemModelProvider { - public Task GetContentItemGenericModelAsync(object item); + /// + /// Builds a model based on given JSON input. + /// + /// Content item data. + /// Universal item + public Task GetUniversalContentItemModelAsync(object item); } } \ No newline at end of file diff --git a/Kontent.Ai.Delivery/ContentItems/Universal/UniversalItemModelProvider.cs b/Kontent.Ai.Delivery/ContentItems/Universal/UniversalItemModelProvider.cs index 97f89c26..819fe26c 100644 --- a/Kontent.Ai.Delivery/ContentItems/Universal/UniversalItemModelProvider.cs +++ b/Kontent.Ai.Delivery/ContentItems/Universal/UniversalItemModelProvider.cs @@ -18,7 +18,7 @@ public UniversalItemModelProvider(JsonSerializer serializer) Serializer = serializer; } - public async Task GetContentItemGenericModelAsync(object item) + public async Task GetUniversalContentItemModelAsync(object item) => (IUniversalContentItem)await GetContentItemModelAsync((JObject)item); internal Task GetContentItemModelAsync(JObject serializedItem) diff --git a/Kontent.Ai.Delivery/DeliveryClient.cs b/Kontent.Ai.Delivery/DeliveryClient.cs index 2acbcaa8..e39e89ed 100644 --- a/Kontent.Ai.Delivery/DeliveryClient.cs +++ b/Kontent.Ai.Delivery/DeliveryClient.cs @@ -337,14 +337,14 @@ public async Task GetUniversalItemAsync(string c } var content = await response.GetJsonContentAsync(); - var model = await UniversalItemModelProvider.GetContentItemGenericModelAsync(content["item"]); + var model = await UniversalItemModelProvider.GetUniversalContentItemModelAsync(content["item"]); var linkedUniversalItems = await Task.WhenAll( content["modular_content"]? .Values() .Select(async linkedItem => { - var model = await UniversalItemModelProvider.GetContentItemGenericModelAsync(linkedItem); + var model = await UniversalItemModelProvider.GetUniversalContentItemModelAsync(linkedItem); return new KeyValuePair(model.System.Codename, model); }) ); @@ -368,14 +368,14 @@ public async Task GetUniversalItemsAsync( var content = await response.GetJsonContentAsync(); var pagination = content["pagination"].ToObject(Serializer); - var items = ((JArray)content["items"]).Select(async source => await UniversalItemModelProvider.GetContentItemGenericModelAsync(source)); + var items = ((JArray)content["items"]).Select(async source => await UniversalItemModelProvider.GetUniversalContentItemModelAsync(source)); var linkedUniversalItems = await Task.WhenAll( content["modular_content"]? .Values() .Select(async linkedItem => { - var model = await UniversalItemModelProvider.GetContentItemGenericModelAsync(linkedItem); + var model = await UniversalItemModelProvider.GetUniversalContentItemModelAsync(linkedItem); return new KeyValuePair(model.System.Codename, model); }) ); diff --git a/docs/retrieving-data/universal-item-retrieval.md b/docs/retrieving-data/universal-item-retrieval.md index 2b2e76a6..e1894e4d 100644 --- a/docs/retrieving-data/universal-item-retrieval.md +++ b/docs/retrieving-data/universal-item-retrieval.md @@ -80,7 +80,7 @@ You can implement your own `IUniversalModelProvider` and register it via `Delive class CustomUniversalItemModelProvider : IUniversalItemModelProvider { // custom implementation - public async Task GetContentItemGenericModelAsync(object item) => CustomImplementation((JObject)item); + public async Task GetUniversalContentItemModelAsync(object item) => CustomImplementation((JObject)item); } var client = DeliveryClientBuilder From dce01c1870b6eb2e5765b09e003485174b2de507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Chrastina?= <9218736+Simply007@users.noreply.github.com> Date: Thu, 20 Apr 2023 13:15:15 +0200 Subject: [PATCH 34/34] Rename IUniversalModelProvider.cs to IUniversalItemModelProvider.cs --- ...UniversalModelProvider.cs => IUniversalItemModelProvider.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/{IUniversalModelProvider.cs => IUniversalItemModelProvider.cs} (99%) diff --git a/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalItemModelProvider.cs similarity index 99% rename from Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs rename to Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalItemModelProvider.cs index d904e7f3..989643d4 100644 --- a/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalModelProvider.cs +++ b/Kontent.Ai.Delivery.Abstractions/ContentItems/Universal/IUniversalItemModelProvider.cs @@ -17,4 +17,4 @@ public interface IUniversalItemModelProvider /// Universal item public Task GetUniversalContentItemModelAsync(object item); } -} \ No newline at end of file +}