Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Asset URL Replacement Enhancement #398

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,14 @@ public class DeliveryOptions
/// </summary>
[Obsolete("#312")]
internal string Name { get; set; }

/// <summary>
/// Replaces the base URL of the asset URLs in the API response. The asset URL will retain all the identifying information such as project id, asset id and file name.<br/>
/// <b>NOTE: </b>Do not specify a trailing backslash in the URL.<br/>
/// <b>Example where AssetUrlReplacement is defined as "https://www.example.com/assets":</b><br/>
/// <b>Original Value:</b> https://preview-assets-us-01.kc-usercontent.com/7ffda7b5-bfb2-4226-8b4f-d2e333638416/614cb0cc-9e62-4572-a408-fc8715f990e8/test-asset.pdf <br/>
/// <b>New Value:</b> https://www.example.com/assets/7ffda7b5-bfb2-4226-8b4f-d2e333638416/614cb0cc-9e62-4572-a408-fc8715f990e8/test-asset.pdf
/// </summary>
public string AssetUrlReplacement { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public static void Configure(this DeliveryOptions o, DeliveryOptions options)
o.Name = options.Name;
#pragma warning restore CS0618
o.DefaultRenditionPreset = options.DefaultRenditionPreset;
o.AssetUrlReplacement = options.AssetUrlReplacement;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -200,5 +200,36 @@ public void BuildWithDefaultRenditionPreset()

Assert.Equal(renditionPreset, deliveryOptions.DefaultRenditionPreset);
}

[Fact]
public void BuildWithAssetUrlReplacement()
{
const string assetUrl = "https://www.example.com/assets";

var deliveryOptions = DeliveryOptionsBuilder
.CreateInstance()
.WithEnvironmentId(EnvironmentId)
.UseProductionApi()
.WithAssetUrlReplacement(assetUrl)
.Build();

Assert.Equal(assetUrl, deliveryOptions.AssetUrlReplacement);
}

[Fact]
public void BuildWithAssetUrlAsUriReplacement()
{
const string assetUrl = "https://www.example.com/assets";
var uri = new Uri(assetUrl, UriKind.Absolute);

var deliveryOptions = DeliveryOptionsBuilder
.CreateInstance()
.WithEnvironmentId(EnvironmentId)
.UseProductionApi()
.WithAssetUrlReplacement(uri)
.Build();

Assert.Equal(assetUrl, deliveryOptions.AssetUrlReplacement);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,21 @@ public void BuildWithEnvironmentId_ReturnsDeliveryClientWithEnvironmentIdSet()
public void BuildWithDeliveryOptions_ReturnsDeliveryClientWithDeliveryOptions()
{
var guid = new Guid(EnvironmentId);
var assetReplacementUrl = "https://cdn.example.com/assets";

var deliveryClient = (Delivery.DeliveryClient) DeliveryClientBuilder
.WithOptions(builder => builder
.WithEnvironmentId(guid)
.UsePreviewApi(PreviewApiKey)
.WithCustomEndpoint(PreviewEndpoint)
.WithAssetUrlReplacement("https://cdn.example.com/assets")
.Build()
).Build();

Assert.Equal(EnvironmentId, deliveryClient.DeliveryOptions.CurrentValue.EnvironmentId);
Assert.True(deliveryClient.DeliveryOptions.CurrentValue.UsePreviewApi);
Assert.Equal(PreviewEndpoint, deliveryClient.DeliveryOptions.CurrentValue.PreviewEndpoint);
Assert.Equal(assetReplacementUrl, deliveryClient.DeliveryOptions.CurrentValue.AssetUrlReplacement);
}

[Fact]
Expand Down
62 changes: 62 additions & 0 deletions Kontent.Ai.Delivery.Tests/ValueConverterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,25 @@ public async Task AssetElementValueConverter_NoPresetSpecifiedInConfig_AssetUrlI

Assert.Equal(assetUrl, teaserImage.Url);
}

[Fact]
public async Task AssetElementValueConverter_NoPresetSpecifiedInConfig_AssetUrlReplacementSpecifiedInConfig_AssetUrlIsTouched()
{
var mockHttp = new MockHttpMessageHandler();
mockHttp
.When($"{_baseUrl}/items/coffee_beverages_explained")
.Respond("application/json", await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}coffee_beverages_explained.json")));

var client = InitializeDeliveryClient(mockHttp);
client.DeliveryOptions.CurrentValue.AssetUrlReplacement = "https://cdn.example.com/assets";

var response = await client.GetItemAsync<Article>("coffee_beverages_explained");
var teaserImage = response.Item.TeaserImage.FirstOrDefault();

var assetUrl = "https://cdn.example.com/assets/975bf280-fd91-488c-994c-2f04416e5ee3/e700596b-03b0-4cee-ac5c-9212762c027a/coffee-beverages-explained-1080px.jpg";

Assert.Equal(assetUrl, teaserImage.Url);
}

[Fact]
public async Task AssetElementValueConverter_DefaultPresetSpecifiedInConfig_AssetUrlContainsDefaultRenditionQuery()
Expand All @@ -159,6 +178,28 @@ public async Task AssetElementValueConverter_DefaultPresetSpecifiedInConfig_Asse

Assert.Equal($"{assetUrl}?{defaultRenditionQuery}", teaserImage.Url);
}

[Fact]
public async Task AssetElementValueConverter_DefaultPresetSpecifiedInConfig_AssetUrlReplacementSpecifiedInConfig_AssetUrlContainsDefaultRenditionQuery()
{
var mockHttp = new MockHttpMessageHandler();
mockHttp
.When($"{_baseUrl}/items/coffee_beverages_explained")
.Respond("application/json", await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}coffee_beverages_explained.json")));

var defaultRenditionPreset = "default";

var client = InitializeDeliveryClient(mockHttp, new DeliveryOptions { EnvironmentId = _guid, DefaultRenditionPreset = defaultRenditionPreset });
client.DeliveryOptions.CurrentValue.AssetUrlReplacement = "https://cdn.example.com/assets";

var response = await client.GetItemAsync<Article>("coffee_beverages_explained");
var teaserImage = response.Item.TeaserImage.FirstOrDefault();

var assetUrl = "https://cdn.example.com/assets/975bf280-fd91-488c-994c-2f04416e5ee3/e700596b-03b0-4cee-ac5c-9212762c027a/coffee-beverages-explained-1080px.jpg";
var defaultRenditionQuery = "w=200&h=150&fit=clip&rect=7,23,300,200";

Assert.Equal($"{assetUrl}?{defaultRenditionQuery}", teaserImage.Url);
}

[Fact]
public async Task AssetElementValueConverter_MobilePresetSpecifiedInConfig_AssetUrlIsUntouchedAsThereIsNoMobileRenditionSpecified()
Expand All @@ -180,6 +221,27 @@ public async Task AssetElementValueConverter_MobilePresetSpecifiedInConfig_Asset
Assert.Equal(assetUrl, teaserImage.Url);
}

[Fact]
public async Task AssetElementValueConverter_MobilePresetSpecifiedInConfig_AssetUrlReplacementSpecifiedInConfig_AssetUrlIsUntouchedAsThereIsNoMobileRenditionSpecified()
{
var mockHttp = new MockHttpMessageHandler();
mockHttp
.When($"{_baseUrl}/items/coffee_beverages_explained")
.Respond("application/json", await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, $"Fixtures{Path.DirectorySeparatorChar}DeliveryClient{Path.DirectorySeparatorChar}coffee_beverages_explained.json")));

var defaultRenditionPreset = "mobile";

var client = InitializeDeliveryClient(mockHttp, new DeliveryOptions { EnvironmentId = _guid, DefaultRenditionPreset = defaultRenditionPreset });
client.DeliveryOptions.CurrentValue.AssetUrlReplacement = "https://cdn.example.com/assets";

var response = await client.GetItemAsync<Article>("coffee_beverages_explained");
var teaserImage = response.Item.TeaserImage.FirstOrDefault();

var assetUrl = "https://cdn.example.com/assets/975bf280-fd91-488c-994c-2f04416e5ee3/e700596b-03b0-4cee-ac5c-9212762c027a/coffee-beverages-explained-1080px.jpg";

Assert.Equal(assetUrl, teaserImage.Url);
}

private DeliveryClient InitializeDeliveryClient(MockHttpMessageHandler mockHttp, DeliveryOptions options = null)
{
var deliveryHttpClient = new DeliveryHttpClient(mockHttp.ToHttpClient());
Expand Down
12 changes: 12 additions & 0 deletions Kontent.Ai.Delivery/Configuration/DeliveryOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ IOptionalDeliveryConfiguration IOptionalDeliveryConfiguration.WithDefaultRenditi
return this;
}

IOptionalDeliveryConfiguration IOptionalDeliveryConfiguration.WithAssetUrlReplacement(string url)
{
_deliveryOptions.AssetUrlReplacement = url;
return this;
}

IOptionalDeliveryConfiguration IOptionalDeliveryConfiguration.WithAssetUrlReplacement(Uri url)
{
_deliveryOptions.AssetUrlReplacement = url.AbsoluteUri;
return this;
}

private void SetCustomEndpoint(string endpoint)
{
if (_deliveryOptions.UsePreviewApi)
Expand Down
22 changes: 22 additions & 0 deletions Kontent.Ai.Delivery/Configuration/IDeliveryOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,28 @@ public interface IOptionalDeliveryConfiguration : IDeliveryOptionsBuild
/// </remarks>
/// <param name="presetCodename">Codename of the rendition preset to be applied automatically.</param>
IOptionalDeliveryConfiguration WithDefaultRenditionPreset(string presetCodename);

/// <summary>
/// Replaces the base URL of the asset URLs in the API response. The asset URL will retain all the identifying information such as project id, asset id and file name.<br/>
/// <b>NOTE: </b>Do not specify a trailing backslash in the URL.<br/>
/// <b>Example where url is defined as "https://www.example.com/assets":</b><br/>
/// <b>Original Value:</b> https://preview-assets-us-01.kc-usercontent.com/7ffda7b5-bfb2-4226-8b4f-d2e333638416/614cb0cc-9e62-4572-a408-fc8715f990e8/test-asset.pdf <br/>
/// <b>New Value:</b> https://www.example.com/assets/7ffda7b5-bfb2-4226-8b4f-d2e333638416/614cb0cc-9e62-4572-a408-fc8715f990e8/test-asset.pdf
/// </summary>
/// <param name="url">URL to substitute for asset urls</param>
/// <returns></returns>
IOptionalDeliveryConfiguration WithAssetUrlReplacement(string url);

/// <summary>
/// Replaces the base URL of the asset URLs in the API response. The asset URL will retain all the identifying information such as project id, asset id and file name.<br/>
/// <b>NOTE: </b>Do not specify a trailing backslash in the URL.<br/>
/// <b>Example where url is defined as "https://www.example.com/assets":</b><br/>
/// <b>Original Value:</b> https://preview-assets-us-01.kc-usercontent.com/7ffda7b5-bfb2-4226-8b4f-d2e333638416/614cb0cc-9e62-4572-a408-fc8715f990e8/test-asset.pdf <br/>
/// <b>New Value:</b> https://www.example.com/assets/7ffda7b5-bfb2-4226-8b4f-d2e333638416/614cb0cc-9e62-4572-a408-fc8715f990e8/test-asset.pdf
/// </summary>
/// <param name="url">URL to substitute for asset urls</param>
/// <returns></returns>
IOptionalDeliveryConfiguration WithAssetUrlReplacement(Uri url);
}

/// <summary>
Expand Down
28 changes: 24 additions & 4 deletions Kontent.Ai.Delivery/ContentItems/AssetElementValueConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,33 @@ public Task<object> GetPropertyValueAsync<TElement>(PropertyInfo property, TElem

private string ResolveAssetUrl(IAsset asset)
{
var url = ReplaceAssetUrlWIthCustomAssetUrl(asset.Url);
var renditionPresetToBeApplied = Options.CurrentValue.DefaultRenditionPreset;
if (renditionPresetToBeApplied == null || asset.Renditions == null)
return asset.Url;
return url;

return asset.Renditions.TryGetValue(renditionPresetToBeApplied, out var renditionToBeApplied)
? $"{asset.Url}?{renditionToBeApplied.Query}"
: asset.Url;
? $"{url}?{renditionToBeApplied.Query}"
: url;
}

/// <summary>
/// Replace the beginning part of the asset URL with the AssetUrlReplacement value.
/// </summary>
/// <param name="url">Original Asset Url</param>
/// <returns>New URL with the CDN URL replaces with AssetUrlReplacement</returns>
private string ReplaceAssetUrlWIthCustomAssetUrl(string url)
{
if (!string.IsNullOrEmpty(Options.CurrentValue.AssetUrlReplacement))
{
// Replace the beginning part of the asset URL with the AssetUrlReplacement value by taking the third forward slash as the ending point for the string replacement
var endOfUrlIndex = url.IndexOf("/", url.IndexOf("/", url.IndexOf("/", 0) + 1) + 1);
if (endOfUrlIndex > 0)
{
return Options.CurrentValue.AssetUrlReplacement + url.Substring(endOfUrlIndex);
}
}
return url;
}
}
}
4 changes: 2 additions & 2 deletions Kontent.Ai.Delivery/ContentItems/ModelProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ private IPropertyValueConverter GetValueConverter(PropertyInfo property)
// Specific type converters
if (typeof(IRichTextContent).IsAssignableFrom(property.PropertyType))
{
return new RichTextContentConverter(HtmlParser);
return new RichTextContentConverter(HtmlParser, DeliveryOptions);
}

if (typeof(IDateTimeContent).IsAssignableFrom(property.PropertyType))
Expand All @@ -389,7 +389,7 @@ private IPropertyValueConverter GetValueConverter(PropertyInfo property)
{
return new AssetElementValueConverter(DeliveryOptions);
}

return null;
}

Expand Down
37 changes: 35 additions & 2 deletions Kontent.Ai.Delivery/ContentItems/RichTextContentConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@
using Kontent.Ai.Delivery.ContentItems.ContentLinks;
using Kontent.Ai.Delivery.ContentItems.RichText;
using Kontent.Ai.Delivery.ContentItems.RichText.Blocks;
using Microsoft.Extensions.Options;

namespace Kontent.Ai.Delivery.ContentItems
{
internal class RichTextContentConverter : IPropertyValueConverter<string>
{
public IHtmlParser Parser { get; }
public IOptionsMonitor<DeliveryOptions> Options { get; }

public RichTextContentConverter(IHtmlParser parser)
public RichTextContentConverter(IHtmlParser parser, IOptionsMonitor<DeliveryOptions> options)
{
Parser = parser;
Options = options;
}

public async Task<object> GetPropertyValueAsync<TElement>(PropertyInfo property, TElement contentElement, ResolvingContext context) where TElement : IContentElementValue<string>
Expand Down Expand Up @@ -56,7 +59,21 @@ public async Task<object> GetPropertyValueAsync<TElement>(PropertyInfo property,
if (img != null)
{
var assetId = Guid.Parse(img.GetAttribute("data-asset-id"));
blocks.Add(element.Images[assetId]);
if (!string.IsNullOrEmpty(Options.CurrentValue.AssetUrlReplacement))
{
var assetToReplace = element.Images[assetId];
var replacedAsset = new InlineImage()
{
Url = ReplaceAssetUrlWIthCustomAssetUrl(assetToReplace.Url),
Description = assetToReplace.Description,
Height = assetToReplace.Height,
Width = assetToReplace.Width,
ImageId = assetToReplace.ImageId
};
blocks.Add(replacedAsset);
}
else
blocks.Add(element.Images[assetId]);
}
}
else
Expand All @@ -67,5 +84,21 @@ public async Task<object> GetPropertyValueAsync<TElement>(PropertyInfo property,

return blocks;
}

/// <summary>
/// Replace the beginning part of the asset URL with the AssetUrlReplacement value.
/// </summary>
/// <param name="url">Original Asset Url</param>
/// <returns>New URL with the CDN URL replaces with AssetUrlReplacement</returns>
private string ReplaceAssetUrlWIthCustomAssetUrl(string url)
{
// Replace the beginning part of the asset URL with the AssetUrlReplacement value by taking the third forward slash as the ending point for the string replacement
var endOfUrlIndex = url.IndexOf("/", url.IndexOf("/", url.IndexOf("/", 0) + 1) + 1);
if (endOfUrlIndex > 0)
{
return Options.CurrentValue.AssetUrlReplacement + url.Substring(endOfUrlIndex);
}
return url;
}
}
}
Loading