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

Child documents from Kontent #23

Merged
merged 4 commits into from
Nov 11, 2020
Merged
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
1 change: 0 additions & 1 deletion Kontent.Statiq.Tests/LinqExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Statiq.Common;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Xunit;

Expand Down
2 changes: 1 addition & 1 deletion Kontent.Statiq.Tests/When_executing_a_Statiq_pipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public async Task It_should_correctly_set_the_default_content_from_richtext()
await engine.ExecuteAsync();
}

public static Engine SetupExecution<TContent>(Kontent<TContent> kontentModule, Func<IReadOnlyList<IDocument>, Task> test ) where TContent : class
private static Engine SetupExecution<TContent>(Kontent<TContent> kontentModule, Func<IReadOnlyList<IDocument>, Task> test ) where TContent : class
{
var engine = new Engine();
var pipeline = new Pipeline()
Expand Down
2 changes: 1 addition & 1 deletion Kontent.Statiq.Tests/When_rendering_a_Razor_view.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public async Task It_should_pickup_Layout_and_view_start()
new MergeContent(modules: new ReadFiles(patterns: Path.GetFullPath(".\\input\\Article.cshtml"))),
// Render the view
new RenderRazor()
.WithModel( Config.FromDocument( (document, context) => document.AsKontent<Models.Article>() ) ),
.WithModel( KontentConfig.As<Article>() ),
new TestModule(async documents => (await documents.First().GetContentStringAsync()).Should().Contain("LAYOUT"))
}
};
Expand Down
141 changes: 141 additions & 0 deletions Kontent.Statiq.Tests/When_working_with_related_documents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using FakeItEasy;
using FluentAssertions;
using Kentico.Kontent.Delivery.Abstractions;
using Kontent.Statiq.Tests.Models;
using Kontent.Statiq.Tests.Tools;
using Statiq.Common;
using Statiq.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace Kontent.Statiq.Tests
{
public class When_working_with_related_documents
{
[Fact]
public async Task It_should_map_child_page_collections()
{
// Arrange
var home = new Home();
var sub1 = CreateArticle("Sub 1");
var sub2 = CreateArticle("Sub 2");
home.Articles = new[] { sub1, sub2 };

var deliveryClient = A.Fake<IDeliveryClient>().WithFakeContent(home);

var sut = new Kontent<Home>(deliveryClient);

// Act
var engine = SetupExecution(sut,
new[]{
new AddDocumentsToMetadata(Keys.Children, KontentConfig.GetChildren<Home>( page => page.Articles ) ),
},
// Assert
async docs => docs.FirstOrDefault().GetChildren().Should().HaveCount(2)
);
await engine.ExecuteAsync();
}

[Fact]
public async Task It_should_allow_multiple_child_page_collections()
{
// Arrange
var home = new Home();
var sub1 = CreateArticle("Sub 1");
var sub2 = CreateArticle("Sub 2");
home.Articles = new[] { sub1, sub2 };
home.Cafes = new[]
{
CreateCafe("Ok Café")
};

var deliveryClient = A.Fake<IDeliveryClient>().WithFakeContent(home);

var sut = new Kontent<Home>(deliveryClient);

// Act
var engine = SetupExecution(sut,
new[]{
new AddKontentDocumentsToMetadata<Home>(page => page.Articles),
new AddKontentDocumentsToMetadata<Home>("cafes", page => page.Cafes),
},

// Assert
async docs =>
{
var articles = docs.FirstOrDefault().GetChildren();
articles.Should().HaveCount(2);
articles.Should().Contain(a => string.Equals(a[KontentKeys.System.Name], "Sub 1"));
articles.Should().Contain(a => string.Equals(a[KontentKeys.System.Name], "Sub 2"));

var cafes = docs.FirstOrDefault().GetChildren("cafes");
cafes.Should().HaveCount(1);
cafes.First()[KontentKeys.System.Name].Should().Be("Ok Café");

});
await engine.ExecuteAsync();
}

[Fact]
public async Task It_should_not_throw_on_null_or_empty_collections()
{
// Arrange
var home = new Home
{
Articles = null!,
Cafes = new Cafe[0]
};

var deliveryClient = A.Fake<IDeliveryClient>().WithFakeContent(home);

var sut = new Kontent<Home>(deliveryClient);

// Act
var engine = SetupExecution(sut,
new[]{
new AddKontentDocumentsToMetadata<Home>(page => page.Articles),
new AddKontentDocumentsToMetadata<Home>("cafes", page => page.Cafes),
},

// Assert
async docs =>
{
var articles = docs.FirstOrDefault().GetChildren();
articles.Should().HaveCount(0);

var cafes = docs.FirstOrDefault().GetChildren("cafes");
cafes.Should().HaveCount(0);
});
await engine.ExecuteAsync();
}

private static Article CreateArticle(string content)
{
var body = new TestRichTextContent {content};

return new Article { BodyCopy = body, System = new TestContentItemSystemAttributes{ Name = content } };
}

private static Cafe CreateCafe(string name)
{
return new Cafe { System = new TestContentItemSystemAttributes{ Name = name }};
}

private static Engine SetupExecution<TContent>(Kontent<TContent> kontentModule, IModule[] processModules, Func<IReadOnlyList<IDocument>, Task> test) where TContent : class
{
var engine = new Engine();
var pipeline = new Pipeline()
{
InputModules = { kontentModule },
OutputModules = { new TestModule(test) }
};
pipeline.ProcessModules.AddRange(processModules);

engine.Pipelines.Add("test", pipeline);
return engine;
}
}
}
43 changes: 43 additions & 0 deletions Kontent.Statiq/AddKontentDocumentsToMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Statiq.Common;
using Statiq.Core;
using System;
using System.Collections.Generic;

namespace Kontent.Statiq
{
/// <summary>
/// Short-hand module for adding Kontent documents as child documents.
/// <para>Use the <see cref="AddDocumentsToMetadata"/> with <see cref="KontentConfig"/> helpers for more control.</para>
/// </summary>
/// <typeparam name="TParent">The content type containing the </typeparam>
public sealed class AddKontentDocumentsToMetadata<TParent> : AddDocumentsToMetadata
{
/// <summary>
/// Add Kontent documents as the default children collection.
/// </summary>
/// <param name="func">A function that returns the related documents from a page.</param>
public AddKontentDocumentsToMetadata(Func<TParent, IEnumerable<object>> func)
: base(Keys.Children, CreateConfig(func))
{

}

/// <summary>
/// Add Kontent documents as metadata.
/// </summary>
/// <param name="key">The metadata key to set</param>
/// <param name="func">A function that returns the related documents from a page.</param>
public AddKontentDocumentsToMetadata(string key, Func<TParent, IEnumerable<object>> func )
: base(key, CreateConfig(func))
{

}

private static Config<IEnumerable<IDocument>> CreateConfig(Func<TParent, IEnumerable<object>> func)
{
if (func == null) throw new ArgumentNullException(nameof(func));

return KontentConfig.GetChildren(func);
}
}
}
5 changes: 5 additions & 0 deletions Kontent.Statiq/Kontent.Statiq.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
<ItemGroup Condition="'$(Configuration)'=='Release'">
<AssemblyAttribute Include="Kentico.Kontent.Delivery.DeliverySourceTrackingHeader" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>$(AssemblyName).Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Kentico.Kontent.Delivery" Version="14.0.0" />
<PackageReference Include="Kentico.Kontent.ImageTransformation" Version="14.0.0" />
Expand Down
37 changes: 2 additions & 35 deletions Kontent.Statiq/Kontent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Module = Statiq.Common.Module;

Expand All @@ -30,49 +29,17 @@ public sealed class Kontent<TContentModel> : Module where TContentModel : class
/// <exception cref="ArgumentNullException">Thrown if <paramref name="client"/> is null.</exception>
public Kontent(IDeliveryClient client)
{
if (client == null)
throw new ArgumentNullException(nameof(client), $"{nameof(client)} must not be null");

_client = client;
_client = client ?? throw new ArgumentNullException(nameof(client), $"{nameof(client)} must not be null");
}

/// <inheritdoc />
protected override async Task<IEnumerable<IDocument>> ExecuteContextAsync(IExecutionContext context)
{
var items = await _client.GetItemsAsync<TContentModel>(QueryParameters);

var documentTasks = items.Items.Select(item => CreateDocument(context, item)).ToArray();
var documentTasks = items.Items.Select(item => KontentDocumentHelpers.CreateDocument(context, item, GetContent)).ToArray();

return await Task.WhenAll(documentTasks);
}

private async Task<IDocument> CreateDocument(IExecutionContext context, TContentModel item)
{
var props = typeof(TContentModel).GetProperties(BindingFlags.Instance | BindingFlags.FlattenHierarchy |
BindingFlags.GetProperty | BindingFlags.Public);
var metadata = new List<KeyValuePair<string, object>>
{
new KeyValuePair<string, object>(TypedContentExtensions.KontentItemKey, item),
};

if (props.FirstOrDefault(prop => typeof(IContentItemSystemAttributes).IsAssignableFrom(prop.PropertyType))
?.GetValue(item) is IContentItemSystemAttributes systemProp)
{
metadata.AddRange(new[]
{
new KeyValuePair<string, object>(KontentKeys.System.Name, systemProp.Name),
new KeyValuePair<string, object>(KontentKeys.System.CodeName, systemProp.Codename),
new KeyValuePair<string, object>(KontentKeys.System.Language, systemProp.Language),
new KeyValuePair<string, object>(KontentKeys.System.Id, systemProp.Id),
new KeyValuePair<string, object>(KontentKeys.System.Type, systemProp.Type),
new KeyValuePair<string, object>(KontentKeys.System.LastModified, systemProp.LastModified)
});
}

var content = GetContent?.Invoke(item) ?? "";

return await context.CreateDocumentAsync(metadata, content, "text/html");
}

}
}
65 changes: 65 additions & 0 deletions Kontent.Statiq/KontentConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Statiq.Common;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Kontent.Statiq
{
/// <summary>
/// Kontent specific Config expressions
/// </summary>
public static class KontentConfig
{
/// <summary>
/// Map related content into a collection of Statiq documents.
/// </summary>
/// <typeparam name="TContentType">The content type.</typeparam>
/// <param name="getChildren">A function that returns a set of Kontent items.</param>
/// <returns>A config object.</returns>
public static Config<IEnumerable<IDocument>> GetChildren<TContentType>(Func<TContentType, IEnumerable<object>> getChildren)
{
if (getChildren == null) throw new ArgumentNullException(nameof(getChildren));

return Config.FromDocument<IEnumerable<IDocument>>( async (doc, ctx) =>
{
var list = new List<IDocument>();
var parent = doc.AsKontent<TContentType>();

if (parent != null)
{
var children = getChildren(parent)?.ToArray() ?? Array.Empty<object>();
foreach (var item in children)
{
list.Add(await KontentDocumentHelpers.CreateDocument(ctx, item, null));
}
}

return list;
});
}

/// <summary>
/// Map a value from a Kontent item.
/// </summary>
/// <typeparam name="TContentType">The Kontent model type.</typeparam>
/// <typeparam name="TValue">The return value.</typeparam>
/// <param name="getValue">A function that retrieves the value from the content.</param>
/// <returns>A config object.</returns>
public static Config<TValue> Get<TContentType, TValue>(Func<TContentType, TValue> getValue)
{
if (getValue == null) throw new ArgumentNullException(nameof(getValue));

return Config.FromDocument((doc, ctx) => getValue(doc.AsKontent<TContentType>()));
}

/// <summary>
/// Map a document from a Kontent item.
/// </summary>
/// <typeparam name="TContentType">The Kontent model type.</typeparam>
/// <returns>A config object.</returns>
public static Config<TContentType> As<TContentType>()
{
return Config.FromDocument((doc, ctx) => doc.AsKontent<TContentType>());
}
}
}
Loading