Skip to content

Commit

Permalink
Support for child documents + general API improvements
Browse files Browse the repository at this point in the history
* KontentConfig makes it a bit easier to work with Statiq Config and Kontent
* Child documents can now be axtracted through KontentConfig.GetChildren, issue #21
* Added AddKontentDocumentsToMetadata module to make it even easier to work with releated content
* Moved mapping from Kontent models to IDocument into KontentDocumentHelpers
  • Loading branch information
alanta committed Nov 9, 2020
1 parent 31f8d18 commit bd78147
Show file tree
Hide file tree
Showing 12 changed files with 340 additions and 40 deletions.
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> FromKontent<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

0 comments on commit bd78147

Please sign in to comment.