Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion Kontent.Ai.Delivery.Abstractions/DeliveryClientExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Kontent.Ai.Delivery.Abstractions
{
Expand Down Expand Up @@ -87,5 +88,29 @@ public static Task<IDeliverySyncInitResponse> PostSyncInitAsync(this IDeliveryCl
{
return client.PostSyncInitAsync(parameters);
}

/// <summary>
/// Returns a feed that is used to traverse through strongly typed parent content items matching the optional filtering parameters.
/// </summary>
/// <param name="client">An instance of the <see cref="IDeliveryClient"/></param>
/// <param name="codename">The codename of a content item.</param>
/// <param name="parameters">An array that contains zero or more query parameters for filtering.</param>
/// <returns>The <see cref="IDeliveryItemsFeed{IUsedInItem}"/> instance that can be used to enumerate through content item parents for the specified item codename. If no query parameters are specified, default language parents are enumerated.</returns>
public static IDeliveryItemsFeed<IUsedInItem> GetItemUsedIn(this IDeliveryClient client, string codename, params IQueryParameter[] parameters)
{
return client.GetItemUsedIn(codename, parameters);
}

/// <summary>
/// Returns a feed that is used to traverse through strongly typed parent content items matching the optional filtering parameters.
/// </summary>
/// <param name="client">An instance of the <see cref="IDeliveryClient"/></param>
/// <param name="codename">The codename of an asset.</param>
/// <param name="parameters">An array that contains zero or more query parameters for filtering.</param>
/// <returns>The <see cref="IDeliveryItemsFeed{IUsedInItem}"/> instance that can be used to enumerate through asset parents for the specified asset codename. If no query parameters are specified, default language parents are enumerated.</returns>
public static IDeliveryItemsFeed<IUsedInItem> GetAssetUsedIn(this IDeliveryClient client, string codename, params IQueryParameter[] parameters)
{
return client.GetAssetUsedIn(codename, parameters);
}
}
}
16 changes: 16 additions & 0 deletions Kontent.Ai.Delivery.Abstractions/IDeliveryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,21 @@ public interface IDeliveryClient
/// </summary>
/// <returns>The <see cref="IDeliverySyncResponse"/> instance that represents the sync response that contains collection of delta updates and continuation token needed for further sync execution.</returns>
Task<IDeliverySyncResponse> GetSyncAsync(string continuationToken);

/// <summary>
/// Returns a feed that is used to traverse through strongly typed parent content items matching the optional filtering parameters.
/// </summary>
/// <param name="codename">The codename of a content item.</param>
/// <param name="parameters">A collection of query parameters for filtering.</param>
/// <returns>The <see cref="IDeliveryItemsFeed{IUsedInItem}"/> instance that can be used to enumerate through content item parents for the specified item codename. If no query parameters are specified, default language parents are enumerated.</returns>
public IDeliveryItemsFeed<IUsedInItem> GetItemUsedIn(string codename, IEnumerable<IQueryParameter> parameters = null);

/// <summary>
/// Returns a feed that is used to traverse through strongly typed parent content items matching the optional filtering parameters.
/// </summary>
/// <param name="codename">The codename of an asset.</param>
/// <param name="parameters">A collection of query parameters for filtering.</param>
/// <returns>The <see cref="IDeliveryItemsFeed{IUsedInItem}"/> instance that can be used to enumerate through asset parents for the specified asset codename. If no query parameters are specified, default language parents are enumerated.</returns>
public IDeliveryItemsFeed<IUsedInItem> GetAssetUsedIn(string codename, IEnumerable<IQueryParameter> parameters = null);
}
}
14 changes: 14 additions & 0 deletions Kontent.Ai.Delivery.Abstractions/UsedIn/IUsedInItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;

namespace Kontent.Ai.Delivery.Abstractions;

/// <summary>
/// Represents a parent content item.
/// </summary>
public interface IUsedInItem
{
/// <summary>
/// Represents system attributes of a parent content item.
/// </summary>
public IUsedInItemSystemAttributes System { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace Kontent.Ai.Delivery.Abstractions
{
/// <summary>
/// Represents system attributes of a parent content item.
/// </summary>
public interface IUsedInItemSystemAttributes : ISystemAttributes
{
/// <summary>
/// Gets the language of the content item.
/// </summary>
string Language { get; }

/// <summary>
/// Gets the codename of the content type, for example "article".
/// </summary>
string Type { get; }

/// <summary>
/// Gets the codename of the content collection to which the content item belongs.
/// </summary>
public string Collection { get; }

/// <summary>
/// Gets the codename of the workflow which the content item is assigned to.
/// </summary>
public string Workflow { get; }

/// <summary>
/// Gets the codename of the workflow step which the content item is assigned to.
/// </summary>
public string WorkflowStep { get; }
}
}
22 changes: 22 additions & 0 deletions Kontent.Ai.Delivery.Caching/DeliveryClientCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,5 +175,27 @@ public Task<IDeliverySyncResponse> GetSyncAsync(string continuationToken)
{
return _deliveryClient.GetSyncAsync(continuationToken);
}

/// <summary>
/// Returns a feed that is used to traverse through strongly typed parent content items matching the optional filtering parameters.
/// </summary>
/// <param name="codename">The codename of a content item.</param>
/// <param name="parameters">A collection of query parameters for filtering.</param>
/// <returns>The <see cref="IDeliveryItemsFeed{IUsedInItem}"/> instance that can be used to enumerate through content item parents for the specified item codename. If no query parameters are specified, default language parents are enumerated.</returns>
public IDeliveryItemsFeed<IUsedInItem> GetItemUsedIn(string codename, IEnumerable<IQueryParameter> parameters = null)
{
return _deliveryClient.GetItemUsedIn(codename, parameters);
}

/// <summary>
/// Returns a feed that is used to traverse through strongly typed parent content items matching the optional filtering parameters.
/// </summary>
/// <param name="codename">The codename of an asset.</param>
/// <param name="parameters">A collection of query parameters for filtering.</param>
/// <returns>The <see cref="IDeliveryItemsFeed{IUsedInItem}"/> instance that can be used to enumerate through asset parents for the specified asset codename. If no query parameters are specified, default language parents are enumerated.</returns>
public IDeliveryItemsFeed<IUsedInItem> GetAssetUsedIn(string codename, IEnumerable<IQueryParameter> parameters = null)
{
return _deliveryClient.GetAssetUsedIn(codename, parameters);
}
}
}
34 changes: 34 additions & 0 deletions Kontent.Ai.Delivery.Rx.Tests/DeliveryObservableProxyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ namespace Kontent.Ai.Delivery.Rx.Tests
public class DeliveryObservableProxyTests
{
private const string BEVERAGES_IDENTIFIER = "coffee_beverages_explained";
private const string ASSET_CODENAME = "asset_codename";
readonly string _guid;
readonly string _baseUrl;
readonly MockHttpMessageHandler _mockHttp;
Expand Down Expand Up @@ -180,6 +181,27 @@ public void LanguagesRetrieved()
Assert.NotEmpty(languages);
Assert.All(languages, language => Assert.NotNull(language.System));
}

[Fact]
public void ItemUsedInRetrieved()
{
var observable = new DeliveryObservableProxy(GetDeliveryClient(MockItemUsedIn)).GetItemUsedInObservable(Article.Codename);
var parents = observable.ToEnumerable().ToList();

Assert.NotEmpty(parents);
Assert.All(parents, item => Assert.NotNull(item.System));
}

[Fact]
public void AssetUsedInRetrieved()
{
var observable = new DeliveryObservableProxy(GetDeliveryClient(MockAssetUsedIn)).GetAssetUsedInObservable(ASSET_CODENAME);
var parents = observable.ToEnumerable().ToList();

Assert.NotEmpty(parents);
Assert.All(parents, item => Assert.NotNull(item.System));
}

public static IOptionsMonitor<DeliveryOptions> CreateMonitor(DeliveryOptions options)
{
var mock = A.Fake<IOptionsMonitor<DeliveryOptions>>();
Expand Down Expand Up @@ -286,6 +308,18 @@ private void MockLanguages()
.Respond("application/json", File.ReadAllText(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}languages.json")));
}

private void MockAssetUsedIn()
{
_mockHttp.When($"{_baseUrl}/assets/{ASSET_CODENAME}/used-in")
.Respond("application/json", File.ReadAllText(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}used_in.json")));
}

private void MockItemUsedIn()
{
_mockHttp.When($"{_baseUrl}/items/{Article.Codename}/used-in")
.Respond("application/json", File.ReadAllText(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}used_in.json")));
}

private static void AssertArticlePropertiesNotNull(Article item)
{
Assert.NotNull(item.System);
Expand Down
30 changes: 30 additions & 0 deletions Kontent.Ai.Delivery.Rx.Tests/Fixtures/used_in.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"items": [
{
"system": {
"id": "35778b62-e97f-42cb-833f-8a34c14e27ce",
"name": "rich text child",
"codename": "rich_text_child",
"language": "language1",
"type": "ct",
"collection": "default",
"workflow": "default",
"workflow_step": "published",
"last_modified": "2024-08-01T14:17:50.4141347Z"
}
},
{
"system": {
"id": "3ad63df6-a439-4a64-8193-14e999bfaaf2",
"name": "rich text child 2",
"codename": "rich_text_child_2",
"language": "language1",
"type": "ct",
"collection": "default",
"workflow": "default",
"workflow_step": "published",
"last_modified": "2024-08-01T14:17:50.4141347Z"
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,10 @@
<ProjectReference Include="..\Kontent.Ai.Delivery.Rx\Kontent.Ai.Delivery.Rx.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="Fixtures\used_in.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
67 changes: 56 additions & 11 deletions Kontent.Ai.Delivery.Rx/DeliveryObservableProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,53 @@ public IObservable<T> GetItemsFeedObservable<T>(params IQueryParameter[] paramet
public IObservable<T> GetItemsFeedObservable<T>(IEnumerable<IQueryParameter> parameters) where T : class
{
var feed = DeliveryClient?.GetItemsFeed<T>(parameters);
return feed == null ? null : EnumerateFeed()?.ToObservable();
return feed == null ? null : EnumerateFeed(feed)?.ToObservable();
}

IEnumerable<T> EnumerateFeed()
{
while (feed.HasMoreResults)
{
foreach (var contentItem in feed.FetchNextBatchAsync().Result.Items)
{
yield return contentItem;
}
}
}
/// <summary>
/// Returns an observable of strongly typed parent content items for specified content item that match the optional filtering parameters. Items are enumerated in batches.
/// </summary>
/// <param name="codename">The codename of a content item.</param>
/// <param name="parameters">A collection of query parameters, for example, for filtering or ordering.</param>
/// <returns>The <see cref="IObservable{T}"/> that represents the parent content items for the specified content item. If no query parameters are specified, parents in default language are returned.</returns>
public IObservable<IUsedInItem> GetItemUsedInObservable(string codename, params IQueryParameter[] parameters)
{
return GetItemUsedInObservable(codename, (IEnumerable<IQueryParameter>)parameters);
}

/// <summary>
/// Returns an observable of strongly typed parent content items for specified content item that match the optional filtering parameters. Items are enumerated in batches.
/// </summary>
/// <param name="codename">The codename of a content item.</param>
/// <param name="parameters">A collection of query parameters, for example, for filtering or ordering.</param>
/// <returns>The <see cref="IObservable{IUsedInItem}"/> that represents the parent content items for the specified content item. If no query parameters are specified, parents in default language are returned.</returns>
public IObservable<IUsedInItem> GetItemUsedInObservable(string codename, IEnumerable<IQueryParameter> parameters)
{
var feed = DeliveryClient?.GetItemUsedIn(codename, parameters);
return feed == null ? null : EnumerateFeed(feed)?.ToObservable();
}

/// <summary>
/// Returns an observable of strongly typed parent content items for specified asset that match the optional filtering parameters. Items are enumerated in batches.
/// </summary>
/// <param name="codename">The codename of an asset.</param>
/// <param name="parameters">A collection of query parameters, for example, for filtering or ordering.</param>
/// <returns>The <see cref="IObservable{T}"/> that represents the parent content items for the specified content item. If no query parameters are specified, parents in default language are returned.</returns>
public IObservable<IUsedInItem> GetAssetUsedInObservable(string codename, params IQueryParameter[] parameters)
{
return GetAssetUsedInObservable(codename, (IEnumerable<IQueryParameter>)parameters);
}

/// <summary>
/// Returns an observable of strongly typed parent content items for specified asset that match the optional filtering parameters. Items are enumerated in batches.
/// </summary>
/// <param name="codename">The codename of an asset.</param>
/// <param name="parameters">A collection of query parameters, for example, for filtering or ordering.</param>
/// <returns>The <see cref="IObservable{IUsedInItem}"/> that represents the parent content items for the specified content item. If no query parameters are specified, parents in default language are returned.</returns>
public IObservable<IUsedInItem> GetAssetUsedInObservable(string codename, IEnumerable<IQueryParameter> parameters)
{
var feed = DeliveryClient?.GetAssetUsedIn(codename, parameters);
return feed == null ? null : EnumerateFeed(feed)?.ToObservable();
}

/// <summary>
Expand Down Expand Up @@ -211,6 +246,16 @@ public IObservable<ILanguage> GetLanguagesObservable(params IQueryParameter[] pa

#endregion
#region "Private methods"
private static IEnumerable<T> EnumerateFeed<T>(IDeliveryItemsFeed<T> feed) where T : class
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method is synchronous but invokes an async method and waits for its result, effectively blocking threads. not sure if this is a valid scenario under the circumstances, but I'm not familiar with Rx so I asked AI about it.

the fix is simple, make the method async and have it return IAsyncEnumerable:

private static async IAsyncEnumerable<T> EnumerateFeed<T>(IDeliveryItemsFeed<T> feed) where T : class
{
    while (feed.HasMoreResults)
    {
        var batch = await feed.FetchNextBatchAsync();
        foreach (var contentItem in batch.Items)
        {
            yield return contentItem;
        }
    }
}

however, related methods would need to be updated as well.

again, I'm not sure if thread locking or even deadlocks can be a thing in our scenario. also, Rx is used much less, compared to async, so I leave this at your decision.

while (feed.HasMoreResults)
{
foreach (var contentItem in feed.FetchNextBatchAsync().Result.Items)
{
yield return contentItem;
}
}
}

private static IObservable<T> GetObservableOfOne<T>(Func<T> responseFactory)
{
Expand Down
Loading
Loading