mirror of https://github.com/Squidex/squidex.git
Browse Source
* Started with liquid support. * First working jint extension. * Finalized liquid support.pull/532/head
committed by
GitHub
55 changed files with 2149 additions and 722 deletions
@ -0,0 +1,67 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Fluid; |
||||
|
using Fluid.Values; |
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Core.Templates; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.HandleRules.Extensions |
||||
|
{ |
||||
|
public sealed class EventFluidExtensions : IFluidExtension |
||||
|
{ |
||||
|
private readonly IUrlGenerator urlGenerator; |
||||
|
|
||||
|
public EventFluidExtensions(IUrlGenerator urlGenerator) |
||||
|
{ |
||||
|
Guard.NotNull(urlGenerator, nameof(urlGenerator)); |
||||
|
|
||||
|
this.urlGenerator = urlGenerator; |
||||
|
} |
||||
|
|
||||
|
public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) |
||||
|
{ |
||||
|
TemplateContext.GlobalFilters.AddFilter("contentUrl", ContentUrl); |
||||
|
TemplateContext.GlobalFilters.AddFilter("assetContentUrl", AssetContentUrl); |
||||
|
} |
||||
|
|
||||
|
private FluidValue ContentUrl(FluidValue input, FilterArguments arguments, TemplateContext context) |
||||
|
{ |
||||
|
if (input is ObjectValue objectValue) |
||||
|
{ |
||||
|
if (context.GetValue("event")?.ToObjectValue() is EnrichedContentEvent contentEvent) |
||||
|
{ |
||||
|
if (objectValue.ToObjectValue() is Guid guid && guid != Guid.Empty) |
||||
|
{ |
||||
|
var result = urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, guid); |
||||
|
|
||||
|
return new StringValue(result); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return NilValue.Empty; |
||||
|
} |
||||
|
|
||||
|
private FluidValue AssetContentUrl(FluidValue input, FilterArguments arguments, TemplateContext context) |
||||
|
{ |
||||
|
if (input is ObjectValue objectValue) |
||||
|
{ |
||||
|
if (objectValue.ToObjectValue() is Guid guid && guid != Guid.Empty) |
||||
|
{ |
||||
|
var result = urlGenerator.AssetContent(guid); |
||||
|
|
||||
|
return new StringValue(result); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return NilValue.Empty; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Fluid; |
||||
|
using Fluid.Values; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Templates.Extensions |
||||
|
{ |
||||
|
public sealed class ContentFluidExtension : IFluidExtension |
||||
|
{ |
||||
|
public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) |
||||
|
{ |
||||
|
FluidValue.SetTypeMapping<JsonObject>(x => new ObjectValue(x)); |
||||
|
FluidValue.SetTypeMapping<JsonArray>(x => new JsonArrayFluidValue(x)); |
||||
|
|
||||
|
memberAccessStrategy.Register<NamedContentData, object?>( |
||||
|
(value, name) => value.GetOrDefault(name)); |
||||
|
|
||||
|
memberAccessStrategy.Register<JsonObject, object?>( |
||||
|
(value, name) => value.GetOrDefault(name)); |
||||
|
|
||||
|
memberAccessStrategy.Register<ContentFieldData, object?>( |
||||
|
(value, name) => value.GetOrDefault(name)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,79 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Fluid; |
||||
|
using Fluid.Values; |
||||
|
using NodaTime; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Templates.Extensions |
||||
|
{ |
||||
|
public class DateTimeFluidExtension : IFluidExtension |
||||
|
{ |
||||
|
public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) |
||||
|
{ |
||||
|
TemplateContext.GlobalFilters.AddFilter("format_date", FormatDate); |
||||
|
|
||||
|
TemplateContext.GlobalFilters.AddFilter("timestamp", FormatTimestamp); |
||||
|
TemplateContext.GlobalFilters.AddFilter("timestamp_sec", FormatTimestampSec); |
||||
|
} |
||||
|
|
||||
|
public static FluidValue FormatTimestamp(FluidValue input, FilterArguments arguments, TemplateContext context) |
||||
|
{ |
||||
|
return FormatDate(input, x => FluidValue.Create(x.ToUnixTimeMilliseconds())); |
||||
|
} |
||||
|
|
||||
|
public static FluidValue FormatTimestampSec(FluidValue input, FilterArguments arguments, TemplateContext context) |
||||
|
{ |
||||
|
return FormatDate(input, x => FluidValue.Create(x.ToUnixTimeMilliseconds() / 1000)); |
||||
|
} |
||||
|
|
||||
|
public static FluidValue FormatDate(FluidValue input, FilterArguments arguments, TemplateContext context) |
||||
|
{ |
||||
|
if (arguments.Count == 1) |
||||
|
{ |
||||
|
return FormatDate(input, x => Format(arguments, x)); |
||||
|
} |
||||
|
|
||||
|
return input; |
||||
|
} |
||||
|
|
||||
|
private static FluidValue FormatDate(FluidValue input, Func<DateTimeOffset, FluidValue> formatter) |
||||
|
{ |
||||
|
switch (input) |
||||
|
{ |
||||
|
case DateTimeValue dateTime: |
||||
|
{ |
||||
|
var value = (DateTimeOffset)dateTime.ToObjectValue(); |
||||
|
|
||||
|
return formatter(value); |
||||
|
} |
||||
|
|
||||
|
case ObjectValue objectValue: |
||||
|
{ |
||||
|
var value = objectValue.ToObjectValue(); |
||||
|
|
||||
|
if (value is Instant instant) |
||||
|
{ |
||||
|
return formatter(instant.ToDateTimeOffset()); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return input; |
||||
|
} |
||||
|
|
||||
|
private static FluidValue Format(FilterArguments arguments, DateTimeOffset value) |
||||
|
{ |
||||
|
var formatted = value.ToString(arguments.At(0).ToStringValue()); |
||||
|
|
||||
|
return new StringValue(formatted); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,106 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Globalization; |
||||
|
using System.IO; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using Fluid; |
||||
|
using Fluid.Values; |
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Templates.Extensions |
||||
|
{ |
||||
|
public sealed class JsonArrayFluidValue : FluidValue |
||||
|
{ |
||||
|
private readonly JsonArray value; |
||||
|
|
||||
|
public override FluidValues Type { get; } = FluidValues.Array; |
||||
|
|
||||
|
public JsonArrayFluidValue(JsonArray value) |
||||
|
{ |
||||
|
this.value = value; |
||||
|
} |
||||
|
|
||||
|
public override bool Equals(FluidValue other) |
||||
|
{ |
||||
|
return other is JsonArrayFluidValue array && array.value.Equals(value); |
||||
|
} |
||||
|
|
||||
|
public override bool ToBooleanValue() |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public override decimal ToNumberValue() |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
public override object ToObjectValue() |
||||
|
{ |
||||
|
return new ObjectValue(value); |
||||
|
} |
||||
|
|
||||
|
public override string ToStringValue() |
||||
|
{ |
||||
|
return value.ToString(); |
||||
|
} |
||||
|
|
||||
|
protected override FluidValue GetValue(string name, TemplateContext context) |
||||
|
{ |
||||
|
switch (name) |
||||
|
{ |
||||
|
case "size": |
||||
|
return NumberValue.Create(value.Count); |
||||
|
|
||||
|
case "first": |
||||
|
if (value.Count > 0) |
||||
|
{ |
||||
|
return Create(value[0]); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
|
||||
|
case "last": |
||||
|
if (value.Count > 0) |
||||
|
{ |
||||
|
return Create(value[^1]); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
return NilValue.Instance; |
||||
|
} |
||||
|
|
||||
|
protected override FluidValue GetIndex(FluidValue index, TemplateContext context) |
||||
|
{ |
||||
|
var i = (int)index.ToNumberValue(); |
||||
|
|
||||
|
if (i >= 0 && i < value.Count) |
||||
|
{ |
||||
|
return Create(value[i]); |
||||
|
} |
||||
|
|
||||
|
return NilValue.Instance; |
||||
|
} |
||||
|
|
||||
|
public override IEnumerable<FluidValue> Enumerate() |
||||
|
{ |
||||
|
foreach (var item in value) |
||||
|
{ |
||||
|
yield return Create(item); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) |
||||
|
{ |
||||
|
writer.Write(value.ToString()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Fluid; |
||||
|
using Fluid.Values; |
||||
|
using Newtonsoft.Json; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Templates.Extensions |
||||
|
{ |
||||
|
public sealed class StringFluidExtension : IFluidExtension |
||||
|
{ |
||||
|
public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) |
||||
|
{ |
||||
|
TemplateContext.GlobalFilters.AddFilter("escape", Escape); |
||||
|
TemplateContext.GlobalFilters.AddFilter("slugify", Slugify); |
||||
|
TemplateContext.GlobalFilters.AddFilter("trim", Trim); |
||||
|
} |
||||
|
|
||||
|
public static FluidValue Slugify(FluidValue input, FilterArguments arguments, TemplateContext context) |
||||
|
{ |
||||
|
if (input is StringValue value) |
||||
|
{ |
||||
|
var result = value.ToStringValue().Slugify(); |
||||
|
|
||||
|
return FluidValue.Create(result); |
||||
|
} |
||||
|
|
||||
|
return input; |
||||
|
} |
||||
|
|
||||
|
public static FluidValue Escape(FluidValue input, FilterArguments arguments, TemplateContext context) |
||||
|
{ |
||||
|
var result = input.ToStringValue(); |
||||
|
|
||||
|
result = JsonConvert.ToString(result); |
||||
|
result = result[1..^1]; |
||||
|
|
||||
|
return FluidValue.Create(result); |
||||
|
} |
||||
|
|
||||
|
public static FluidValue Trim(FluidValue input, FilterArguments arguments, TemplateContext context) |
||||
|
{ |
||||
|
return FluidValue.Create(input.ToStringValue().Trim()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Fluid; |
||||
|
using Fluid.Values; |
||||
|
using Squidex.Shared.Users; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Templates.Extensions |
||||
|
{ |
||||
|
public sealed class UserFluidExtension : IFluidExtension |
||||
|
{ |
||||
|
public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) |
||||
|
{ |
||||
|
FluidValue.SetTypeMapping<IUser>(x => new UserFluidValue(x)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,75 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using Fluid; |
||||
|
using Fluid.Values; |
||||
|
using Squidex.Shared.Users; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Templates.Extensions |
||||
|
{ |
||||
|
public sealed class UserFluidValue : FluidValue |
||||
|
{ |
||||
|
private readonly IUser value; |
||||
|
|
||||
|
public override FluidValues Type { get; } = FluidValues.Object; |
||||
|
|
||||
|
public UserFluidValue(IUser value) |
||||
|
{ |
||||
|
this.value = value; |
||||
|
} |
||||
|
|
||||
|
protected override FluidValue GetValue(string name, TemplateContext context) |
||||
|
{ |
||||
|
switch (name) |
||||
|
{ |
||||
|
case "id": |
||||
|
return Create(value.Id); |
||||
|
case "email": |
||||
|
return Create(value.Email); |
||||
|
case "name": |
||||
|
return Create(value.DisplayName()); |
||||
|
default: |
||||
|
return Create(value.Claims.FirstOrDefault(x => string.Equals(name, x.Type, StringComparison.OrdinalIgnoreCase))?.Value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override bool Equals(FluidValue other) |
||||
|
{ |
||||
|
return other is UserFluidValue user && user.value.Id == value.Id; |
||||
|
} |
||||
|
|
||||
|
public override bool ToBooleanValue() |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public override decimal ToNumberValue() |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
public override object ToObjectValue() |
||||
|
{ |
||||
|
return new UserFluidValue(value); |
||||
|
} |
||||
|
|
||||
|
public override string ToStringValue() |
||||
|
{ |
||||
|
return value.Id; |
||||
|
} |
||||
|
|
||||
|
public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) |
||||
|
{ |
||||
|
writer.Write(value.Id); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,93 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Fluid; |
||||
|
using Fluid.Values; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Templates |
||||
|
{ |
||||
|
public sealed class FluidTemplateEngine : ITemplateEngine |
||||
|
{ |
||||
|
private readonly IEnumerable<IFluidExtension> extensions; |
||||
|
|
||||
|
private sealed class SquidexTemplate : BaseFluidTemplate<SquidexTemplate> |
||||
|
{ |
||||
|
public static void Setup(IEnumerable<IFluidExtension> extensions) |
||||
|
{ |
||||
|
foreach (var extension in extensions) |
||||
|
{ |
||||
|
extension.RegisterLanguageExtensions(Factory); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void SetupTypes(IEnumerable<IFluidExtension> extensions) |
||||
|
{ |
||||
|
var globalTypes = TemplateContext.GlobalMemberAccessStrategy; |
||||
|
|
||||
|
globalTypes.MemberNameStrategy = MemberNameStrategies.CamelCase; |
||||
|
|
||||
|
foreach (var extension in extensions) |
||||
|
{ |
||||
|
extension.RegisterGlobalTypes(globalTypes); |
||||
|
} |
||||
|
|
||||
|
foreach (var type in SquidexCoreModel.Assembly.GetTypes().Where(x => x.IsEnum)) |
||||
|
{ |
||||
|
FluidValue.SetTypeMapping(type, x => new StringValue(x.ToString())); |
||||
|
} |
||||
|
|
||||
|
globalTypes.Register<NamedId<Guid>>(); |
||||
|
globalTypes.Register<NamedId<string>>(); |
||||
|
globalTypes.Register<NamedId<long>>(); |
||||
|
globalTypes.Register<RefToken>(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public FluidTemplateEngine(IEnumerable<IFluidExtension> extensions) |
||||
|
{ |
||||
|
Guard.NotNull(extensions, nameof(extensions)); |
||||
|
|
||||
|
this.extensions = extensions; |
||||
|
|
||||
|
SquidexTemplate.Setup(extensions); |
||||
|
SquidexTemplate.SetupTypes(extensions); |
||||
|
} |
||||
|
|
||||
|
public async Task<string> RenderAsync(string template, TemplateVars variables) |
||||
|
{ |
||||
|
Guard.NotNull(variables, nameof(variables)); |
||||
|
|
||||
|
if (SquidexTemplate.TryParse(template, out var parsed, out var errors)) |
||||
|
{ |
||||
|
var context = new TemplateContext(); |
||||
|
|
||||
|
foreach (var extension in extensions) |
||||
|
{ |
||||
|
extension.BeforeRun(context); |
||||
|
} |
||||
|
|
||||
|
foreach (var (key, value) in variables) |
||||
|
{ |
||||
|
context.MemberAccessStrategy.Register(value.GetType()); |
||||
|
|
||||
|
context.SetValue(key, value); |
||||
|
} |
||||
|
|
||||
|
var result = await parsed.RenderAsync(context); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
throw new TemplateParseException(template, errors); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Fluid; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Templates |
||||
|
{ |
||||
|
public interface IFluidExtension |
||||
|
{ |
||||
|
void RegisterLanguageExtensions(FluidParserFactory factory) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
void BeforeRun(TemplateContext templateContext) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Templates |
||||
|
{ |
||||
|
public interface ITemplateEngine |
||||
|
{ |
||||
|
Task<string> RenderAsync(string template, TemplateVars variables); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Runtime.Serialization; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Templates |
||||
|
{ |
||||
|
[Serializable] |
||||
|
public class TemplateParseException : Exception |
||||
|
{ |
||||
|
public IReadOnlyList<string> Errors { get; } |
||||
|
|
||||
|
public TemplateParseException(string template, IEnumerable<string> errors) |
||||
|
: base(BuildErrorMessage(errors, template)) |
||||
|
{ |
||||
|
Errors = errors.ToList(); |
||||
|
} |
||||
|
|
||||
|
protected TemplateParseException(SerializationInfo info, StreamingContext context) |
||||
|
: base(info, context) |
||||
|
{ |
||||
|
Errors = (info.GetValue(nameof(Errors), typeof(List<string>)) as List<string>) ?? new List<string>(); |
||||
|
} |
||||
|
|
||||
|
public override void GetObjectData(SerializationInfo info, StreamingContext context) |
||||
|
{ |
||||
|
info.AddValue(nameof(Errors), Errors.ToList()); |
||||
|
} |
||||
|
|
||||
|
private static string BuildErrorMessage(IEnumerable<string> errors, string template) |
||||
|
{ |
||||
|
var sb = new StringBuilder(); |
||||
|
|
||||
|
sb.AppendLine("Failed to parse template"); |
||||
|
|
||||
|
foreach (var error in errors) |
||||
|
{ |
||||
|
sb.Append(" * "); |
||||
|
sb.AppendLine(error); |
||||
|
} |
||||
|
|
||||
|
sb.AppendLine(); |
||||
|
sb.AppendLine("Template:"); |
||||
|
sb.AppendLine(template); |
||||
|
|
||||
|
return sb.ToString(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Templates |
||||
|
{ |
||||
|
public sealed class TemplateVars : Dictionary<string, object> |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,99 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using System.Threading.Tasks; |
||||
|
using Fluid; |
||||
|
using Fluid.Ast; |
||||
|
using Fluid.Tags; |
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Core.Templates; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public sealed class ReferencesFluidExtension : IFluidExtension |
||||
|
{ |
||||
|
private readonly IContentQueryService contentQueryService; |
||||
|
private readonly IAppProvider appProvider; |
||||
|
|
||||
|
private sealed class ReferenceTag : ArgumentsTag |
||||
|
{ |
||||
|
private readonly IContentQueryService contentQueryService; |
||||
|
private readonly IAppProvider appProvider; |
||||
|
|
||||
|
public ReferenceTag(IContentQueryService contentQueryService, IAppProvider appProvider) |
||||
|
{ |
||||
|
this.contentQueryService = contentQueryService; |
||||
|
|
||||
|
this.appProvider = appProvider; |
||||
|
} |
||||
|
|
||||
|
public override async ValueTask<Completion> WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context, FilterArgument[] arguments) |
||||
|
{ |
||||
|
if (arguments.Length == 2 && context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent) |
||||
|
{ |
||||
|
var app = await appProvider.GetAppAsync(enrichedEvent.AppId.Id); |
||||
|
|
||||
|
if (app == null) |
||||
|
{ |
||||
|
return Completion.Normal; |
||||
|
} |
||||
|
|
||||
|
var appContext = |
||||
|
Context.Admin() |
||||
|
.WithoutContentEnrichment() |
||||
|
.WithoutCleanup() |
||||
|
.WithUnpublished(); |
||||
|
|
||||
|
appContext.App = app; |
||||
|
|
||||
|
var id = (await arguments[1].Expression.EvaluateAsync(context)).ToStringValue(); |
||||
|
|
||||
|
if (Guid.TryParse(id, out var guid)) |
||||
|
{ |
||||
|
var references = await contentQueryService.QueryAsync(appContext, new List<Guid> { guid }); |
||||
|
var reference = references.FirstOrDefault(); |
||||
|
|
||||
|
if (reference != null) |
||||
|
{ |
||||
|
var name = (await arguments[0].Expression.EvaluateAsync(context)).ToStringValue(); |
||||
|
|
||||
|
context.SetValue(name, reference); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return Completion.Normal; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public ReferencesFluidExtension(IContentQueryService contentQueryService, IAppProvider appProvider) |
||||
|
{ |
||||
|
Guard.NotNull(contentQueryService, nameof(contentQueryService)); |
||||
|
Guard.NotNull(appProvider, nameof(appProvider)); |
||||
|
|
||||
|
this.contentQueryService = contentQueryService; |
||||
|
|
||||
|
this.appProvider = appProvider; |
||||
|
} |
||||
|
|
||||
|
public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) |
||||
|
{ |
||||
|
memberAccessStrategy.Register<IContentEntity>(); |
||||
|
} |
||||
|
|
||||
|
public void RegisterLanguageExtensions(FluidParserFactory factory) |
||||
|
{ |
||||
|
factory.RegisterTag("reference", new ReferenceTag(contentQueryService, appProvider)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Reflection; |
||||
|
using Xunit.Sdk; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Operations.HandleRules |
||||
|
{ |
||||
|
public sealed class ExpressionsAttribute : DataAttribute |
||||
|
{ |
||||
|
private readonly string? script; |
||||
|
private readonly string? interpolationOld; |
||||
|
private readonly string? interpolationNew; |
||||
|
private readonly string? liquid; |
||||
|
|
||||
|
public ExpressionsAttribute(string? interpolationOld, string? interpolationNew, string? script, string? liquid) |
||||
|
{ |
||||
|
this.liquid = liquid; |
||||
|
|
||||
|
this.interpolationOld = interpolationOld; |
||||
|
this.interpolationNew = interpolationNew; |
||||
|
|
||||
|
this.script = script; |
||||
|
} |
||||
|
|
||||
|
public override IEnumerable<object[]> GetData(MethodInfo testMethod) |
||||
|
{ |
||||
|
if (interpolationOld != null) |
||||
|
{ |
||||
|
yield return new object[] { interpolationOld }; |
||||
|
} |
||||
|
|
||||
|
if (interpolationNew != null) |
||||
|
{ |
||||
|
yield return new object[] { interpolationNew }; |
||||
|
} |
||||
|
|
||||
|
if (script != null) |
||||
|
{ |
||||
|
yield return new object[] |
||||
|
{ |
||||
|
string.Format("Script(`{0}`)", script) |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
if (liquid != null) |
||||
|
{ |
||||
|
yield return new object[] |
||||
|
{ |
||||
|
string.Format("Liquid({0})", liquid) |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,793 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Core.Assets; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.HandleRules; |
||||
|
using Squidex.Domain.Apps.Core.HandleRules.Extensions; |
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Core.Scripting; |
||||
|
using Squidex.Domain.Apps.Core.Scripting.Extensions; |
||||
|
using Squidex.Domain.Apps.Core.Templates; |
||||
|
using Squidex.Domain.Apps.Core.Templates.Extensions; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
using Squidex.Shared.Identity; |
||||
|
using Squidex.Shared.Users; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Operations.HandleRules |
||||
|
{ |
||||
|
public class RuleEventFormatterCompareTests |
||||
|
{ |
||||
|
private readonly IUser user = A.Fake<IUser>(); |
||||
|
private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>(); |
||||
|
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); |
||||
|
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); |
||||
|
private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); |
||||
|
private readonly Guid contentId = Guid.NewGuid(); |
||||
|
private readonly Guid assetId = Guid.NewGuid(); |
||||
|
private readonly RuleEventFormatter sut; |
||||
|
|
||||
|
private class FakeContentResolver : IRuleEventFormatter |
||||
|
{ |
||||
|
public (bool Match, ValueTask<string?>) Format(EnrichedEvent @event, object value, string[] path) |
||||
|
{ |
||||
|
if (path[0] == "data" && value is JsonArray _) |
||||
|
{ |
||||
|
return (true, GetValueAsync()); |
||||
|
} |
||||
|
|
||||
|
return default; |
||||
|
} |
||||
|
|
||||
|
private async ValueTask<string?> GetValueAsync() |
||||
|
{ |
||||
|
await Task.Delay(5); |
||||
|
|
||||
|
return "Reference"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public RuleEventFormatterCompareTests() |
||||
|
{ |
||||
|
A.CallTo(() => urlGenerator.ContentUI(appId, schemaId, contentId)) |
||||
|
.Returns("content-url"); |
||||
|
|
||||
|
A.CallTo(() => urlGenerator.AssetContent(assetId)) |
||||
|
.Returns("asset-content-url"); |
||||
|
|
||||
|
A.CallTo(() => user.Id) |
||||
|
.Returns("user123"); |
||||
|
|
||||
|
A.CallTo(() => user.Email) |
||||
|
.Returns("me@email.com"); |
||||
|
|
||||
|
A.CallTo(() => user.Claims) |
||||
|
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "me") }); |
||||
|
|
||||
|
JintScriptEngine scriptEngine = BuildScriptEngine(); |
||||
|
|
||||
|
var formatters = new IRuleEventFormatter[] |
||||
|
{ |
||||
|
new PredefinedPatternsFormatter(urlGenerator), |
||||
|
new FakeContentResolver() |
||||
|
}; |
||||
|
|
||||
|
sut = new RuleEventFormatter(TestUtils.DefaultSerializer, formatters, BuildTemplateEngine(), BuildScriptEngine()); |
||||
|
} |
||||
|
|
||||
|
private FluidTemplateEngine BuildTemplateEngine() |
||||
|
{ |
||||
|
var extensions = new IFluidExtension[] |
||||
|
{ |
||||
|
new ContentFluidExtension(), |
||||
|
new DateTimeFluidExtension(), |
||||
|
new EventFluidExtensions(urlGenerator), |
||||
|
new StringFluidExtension(), |
||||
|
new UserFluidExtension() |
||||
|
}; |
||||
|
|
||||
|
return new FluidTemplateEngine(extensions); |
||||
|
} |
||||
|
|
||||
|
private JintScriptEngine BuildScriptEngine() |
||||
|
{ |
||||
|
var extensions = new IJintExtension[] |
||||
|
{ |
||||
|
new DateTimeJintExtension(), |
||||
|
new EventJintExtension(urlGenerator), |
||||
|
new StringJintExtension() |
||||
|
}; |
||||
|
|
||||
|
var cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); |
||||
|
|
||||
|
return new JintScriptEngine(cache, extensions); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"Name $APP_NAME has id $APP_ID", |
||||
|
"Name ${EVENT_APPID.NAME} has id ${EVENT_APPID.ID}", |
||||
|
"Name ${event.appId.name} has id ${event.appId.id}", |
||||
|
"Name {{event.appId.name}} has id {{event.appId.id}}" |
||||
|
)] |
||||
|
public async Task Should_format_app_information_from_event(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent { AppId = appId }; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal($"Name my-app has id {appId.Id}", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"Name $SCHEMA_NAME has id $SCHEMA_ID", |
||||
|
"Name ${EVENT_SCHEMAID.NAME} has id ${EVENT_SCHEMAID.ID}", |
||||
|
"Name ${event.schemaId.name} has id ${event.schemaId.id}", |
||||
|
"Name {{event.schemaId.name}} has id {{event.schemaId.id}}" |
||||
|
)] |
||||
|
public async Task Should_format_schema_information_from_event(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent { SchemaId = schemaId }; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal($"Name my-schema has id {schemaId.Id}", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"DateTime: $TIMESTAMP_DATETIME", |
||||
|
null, |
||||
|
"DateTime: ${formatDate(event.timestamp, 'yyyy-MM-dd-hh-mm-ss')}", |
||||
|
"DateTime: {{event.timestamp | format_date: 'yyyy-MM-dd-hh-mm-ss'}}" |
||||
|
)] |
||||
|
public async Task Should_format_timestamp_information_from_event(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent { Timestamp = now }; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal($"DateTime: {now:yyyy-MM-dd-hh-mm-ss}", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"Date: $TIMESTAMP_DATE", |
||||
|
null, |
||||
|
"Date: ${formatDate(event.timestamp, 'yyyy-MM-dd')}", |
||||
|
"Date: {{event.timestamp | format_date: 'yyyy-MM-dd'}}" |
||||
|
)] |
||||
|
public async Task Should_format_timestamp_date_information_from_event(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent { Timestamp = now }; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal($"Date: {now:yyyy-MM-dd}", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"From $MENTIONED_NAME ($MENTIONED_EMAIL, $MENTIONED_ID)", |
||||
|
"From ${EVENT_MENTIONEDUSER.NAME} (${EVENT_MENTIONEDUSER.EMAIL}, ${EVENT_MENTIONEDUSER.ID})", |
||||
|
"From ${event.mentionedUser.name} (${event.mentionedUser.email}, ${event.mentionedUser.id})", |
||||
|
"From {{event.mentionedUser.name}} ({{event.mentionedUser.email}}, {{event.mentionedUser.id}})" |
||||
|
)] |
||||
|
public async Task Should_format_email_and_display_name_from_mentioned_user(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedCommentEvent { MentionedUser = user }; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("From me (me@email.com, user123)", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"From $USER_NAME ($USER_EMAIL, $USER_ID)", |
||||
|
"From ${EVENT_USER.NAME} (${EVENT_USER.EMAIL}, ${EVENT_USER.ID})", |
||||
|
"From ${event.user.name} (${event.user.email}, ${event.user.id})", |
||||
|
"From {{event.user.name}} ({{event.user.email}}, {{event.user.id}})" |
||||
|
)] |
||||
|
public async Task Should_format_email_and_display_name_from_user(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent { User = user }; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("From me (me@email.com, user123)", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"From $USER_NAME ($USER_EMAIL, $USER_ID)", |
||||
|
"From ${EVENT_USER.NAME} (${EVENT_USER.EMAIL}, ${EVENT_USER.ID})", |
||||
|
"From ${event.user.name} (${event.user.email}, ${event.user.id})", |
||||
|
"From {{event.user.name | default: 'null'}} ({{event.user.email | default: 'null'}}, {{event.user.id | default: 'null'}})" |
||||
|
)] |
||||
|
public async Task Should_return_null_if_user_is_not_found(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent(); |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("From null (null, null)", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"From $USER_NAME ($USER_EMAIL, $USER_ID)", |
||||
|
"From ${EVENT_USER.NAME} (${EVENT_USER.EMAIL}, ${EVENT_USER.ID})", |
||||
|
"From ${event.user.name} (${event.user.email}, ${event.user.id})", |
||||
|
"From {{event.user.name}} ({{event.user.email}}, {{event.user.id}})" |
||||
|
)] |
||||
|
public async Task Should_format_email_and_display_name_from_client(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent { User = new ClientUser(new RefToken(RefTokenType.Client, "android")) }; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("From client:android (client:android, android)", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"Version: $ASSET_VERSION", |
||||
|
"Version: ${EVENT_VERSION}", |
||||
|
"Version: ${event.version}", |
||||
|
"Version: {{event.version}}" |
||||
|
)] |
||||
|
public async Task Should_format_base_property(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedAssetEvent { Version = 13 }; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("Version: 13", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"File: $ASSET_FILENAME", |
||||
|
"File: ${EVENT_FILENAME}", |
||||
|
"File: ${event.fileName}", |
||||
|
"File: {{event.fileName}}" |
||||
|
)] |
||||
|
public async Task Should_format_asset_file_name_from_event(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedAssetEvent { FileName = "my-file.png" }; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("File: my-file.png", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"Type: $ASSSET_ASSETTYPE", |
||||
|
"Type: ${EVENT_ASSETTYPE}", |
||||
|
"Type: ${event.assetType}", |
||||
|
"Type: {{event.assetType}}" |
||||
|
)] |
||||
|
public async Task Should_format_asset_asset_type_from_event(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedAssetEvent { AssetType = AssetType.Audio }; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("Type: Audio", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"Download at $ASSET_CONTENT_URL", |
||||
|
null, |
||||
|
"Download at ${assetContentUrl()}", |
||||
|
"Download at {{event.id | assetContentUrl}}" |
||||
|
)] |
||||
|
public async Task Should_format_asset_content_url_from_event(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedAssetEvent { Id = assetId }; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("Download at asset-content-url", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"Download at $ASSET_CONTENT_URL", |
||||
|
null, |
||||
|
"Download at ${assetContentUrl()}", |
||||
|
"Download at {{event.id | assetContentUrl | default: 'null'}}" |
||||
|
)] |
||||
|
public async Task Should_return_null_when_asset_content_url_not_found(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent(); |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("Download at null", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"Go to $CONTENT_URL", |
||||
|
null, |
||||
|
"Go to ${contentUrl()}", |
||||
|
"Go to {{event.id | contentUrl | default: 'null'}}" |
||||
|
)] |
||||
|
public async Task Should_format_content_url_from_event(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent { AppId = appId, Id = contentId, SchemaId = schemaId }; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("Go to content-url", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"Go to $CONTENT_URL", |
||||
|
null, |
||||
|
"Go to ${contentUrl()}", |
||||
|
"Go to {{event.id | contentUrl | default: 'null'}}" |
||||
|
)] |
||||
|
public async Task Should_return_null_when_content_url_when_not_found(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedAssetEvent(); |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("Go to null", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"$CONTENT_STATUS", |
||||
|
"${EVENT_STATUS}", |
||||
|
"${contentAction()}", |
||||
|
"{{event.status}}" |
||||
|
)] |
||||
|
public async Task Should_format_content_status_when_found(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent { Status = Status.Published }; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("Published", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"$CONTENT_STATUS", |
||||
|
"${EVENT_STATUS}", |
||||
|
"${contentAction()}", |
||||
|
"{{event.status | default: 'null'}}" |
||||
|
)] |
||||
|
public async Task Should_return_null_when_content_status_not_found(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedAssetEvent(); |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("null", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"$CONTENT_ACTION", |
||||
|
"${EVENT_TYPE}", |
||||
|
"${event.type}", |
||||
|
"{{event.type}}" |
||||
|
)] |
||||
|
public async Task Should_format_content_actions_when_found(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent { Type = EnrichedContentEventType.Created }; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("Created", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"$CONTENT_STATUS", |
||||
|
"${CONTENT_STATUS}", |
||||
|
"${contentAction()}", |
||||
|
null |
||||
|
)] |
||||
|
public async Task Should_return_null_when_content_action_not_found(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedAssetEvent(); |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("null", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"$CONTENT_DATA.country.iv", |
||||
|
"${CONTENT_DATA.country.iv}", |
||||
|
"${event.data.country.iv}", |
||||
|
"{{event.data.country.iv | default: 'null'}}" |
||||
|
)] |
||||
|
public async Task Should_return_null_when_field_not_found(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", |
||||
|
new ContentFieldData() |
||||
|
.AddValue("iv", "Berlin")) |
||||
|
}; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("null", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"$CONTENT_DATA.country.iv", |
||||
|
"${CONTENT_DATA.country.iv}", |
||||
|
"${event.data.country.iv}", |
||||
|
"{{event.data.country.iv | default: 'null'}}" |
||||
|
)] |
||||
|
public async Task Should_return_null_when_partition_not_found(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", |
||||
|
new ContentFieldData() |
||||
|
.AddValue("iv", "Berlin")) |
||||
|
}; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("null", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"$CONTENT_DATA.country.iv.10", |
||||
|
"${CONTENT_DATA.country.iv.10}", |
||||
|
"${event.data.country.iv[10]}", |
||||
|
"{{event.data.country.iv[10] | default: 'null'}}" |
||||
|
)] |
||||
|
public async Task Should_return_null_when_array_item_not_found(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", |
||||
|
new ContentFieldData() |
||||
|
.AddJsonValue(JsonValue.Array())) |
||||
|
}; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("null", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"$CONTENT_DATA.country.iv.Location", |
||||
|
"${CONTENT_DATA.country.iv.Location}", |
||||
|
"${event.data.country.iv.Location}", |
||||
|
"{{event.data.country.iv.Location | default: 'null'}}" |
||||
|
)] |
||||
|
public async Task Should_return_null_when_property_not_found(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", |
||||
|
new ContentFieldData() |
||||
|
.AddJsonValue(JsonValue.Object().Add("name", "Berlin"))) |
||||
|
}; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("null", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"$CONTENT_DATA.city.iv", |
||||
|
"${CONTENT_DATA.city.iv}", |
||||
|
"${event.data.city.iv}", |
||||
|
"{{event.data.city.iv}}" |
||||
|
)] |
||||
|
public async Task Should_return_plain_value_when_found(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", |
||||
|
new ContentFieldData() |
||||
|
.AddValue("iv", "Berlin")) |
||||
|
}; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("Berlin", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"$CONTENT_DATA.city.iv.0", |
||||
|
"${CONTENT_DATA.city.iv.0}", |
||||
|
"${event.data.city.iv[0]}", |
||||
|
"{{event.data.city.iv[0]}}" |
||||
|
)] |
||||
|
public async Task Should_return_plain_value_from_array_when_found(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", |
||||
|
new ContentFieldData() |
||||
|
.AddJsonValue(JsonValue.Array("Berlin"))) |
||||
|
}; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("Berlin", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"$CONTENT_DATA.city.iv.name", |
||||
|
"${CONTENT_DATA.city.iv.name}", |
||||
|
"${event.data.city.iv.name}", |
||||
|
"{{event.data.city.iv.name}}" |
||||
|
)] |
||||
|
public async Task Should_return_plain_value_from_object_when_found(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", |
||||
|
new ContentFieldData() |
||||
|
.AddJsonValue(JsonValue.Object().Add("name", "Berlin"))) |
||||
|
}; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("Berlin", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"$CONTENT_DATA.city.iv", |
||||
|
"${CONTENT_DATA.city.iv}", |
||||
|
"${JSON.stringify(event.data.city.iv)}", |
||||
|
"{{event.data.city.iv}}" |
||||
|
)] |
||||
|
public async Task Should_return_json_string_when_object(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", |
||||
|
new ContentFieldData() |
||||
|
.AddJsonValue(JsonValue.Object().Add("name", "Berlin"))) |
||||
|
}; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("{\"name\":\"Berlin\"}", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"$CONTENT_DATA.city.iv", |
||||
|
"${CONTENT_DATA.city.iv}", |
||||
|
"${JSON.stringify(event.data.city.iv)}", |
||||
|
"{{event.data.city.iv}}" |
||||
|
)] |
||||
|
public async Task Should_return_json_string_when_array(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("city", |
||||
|
new ContentFieldData() |
||||
|
.AddJsonValue(JsonValue.Array(1, 2, 3))) |
||||
|
}; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("[1,2,3]", result?.Replace(" ", string.Empty)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
null, |
||||
|
"From ${EVENT_ACTOR}", |
||||
|
"From ${event.actor}", |
||||
|
"From {{event.actor}}" |
||||
|
)] |
||||
|
public async Task Should_format_actor(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent { Actor = new RefToken(RefTokenType.Client, "android") }; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("From client:android", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
null, |
||||
|
"${ASSET_LASTMODIFIED | timestamp}", |
||||
|
"${event.lastModified.getTime()}", |
||||
|
"{{event.lastModified | timestamp}}" |
||||
|
)] |
||||
|
public async Task Should_transform_timestamp(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedAssetEvent { LastModified = Instant.FromUnixTimeSeconds(1590769584) }; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("1590769584000", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
null, |
||||
|
"${ASSET_LASTMODIFIED | timestamp_sec}", |
||||
|
"${event.lastModified.getTime() / 1000}", |
||||
|
"{{event.lastModified | timestamp_sec}}" |
||||
|
)] |
||||
|
public async Task Should_transform_timestamp_seconds(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedAssetEvent { LastModified = Instant.FromUnixTimeSeconds(1590769584) }; |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("1590769584", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"${USER_NAME | Upper}", |
||||
|
"${EVENT_USER.NAME | Upper}", |
||||
|
"${event.user.name.toUpperCase()}", |
||||
|
"{{event.user.name | upcase}}" |
||||
|
)] |
||||
|
public async Task Should_transform_upper(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent { User = user }; |
||||
|
|
||||
|
A.CallTo(() => user.Claims) |
||||
|
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck") }); |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("DONALD DUCK", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"${USER_NAME | Lower}", |
||||
|
"${EVENT_USER.NAME | Lower}", |
||||
|
"${event.user.name.toLowerCase()}", |
||||
|
"{{event.user.name | downcase}}" |
||||
|
)] |
||||
|
public async Task Should_transform_lower(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent { User = user }; |
||||
|
|
||||
|
A.CallTo(() => user.Claims) |
||||
|
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck") }); |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("donald duck", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"${USER_NAME | Trim}", |
||||
|
"${EVENT_USER.NAME | Trim}", |
||||
|
"${event.user.name.trim()}", |
||||
|
"{{event.user.name | trim}}" |
||||
|
)] |
||||
|
public async Task Should_transform_trimmed(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent { User = user }; |
||||
|
|
||||
|
A.CallTo(() => user.Claims) |
||||
|
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck ") }); |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("Donald Duck", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"${USER_NAME | Slugify}", |
||||
|
"${EVENT_USER.NAME | Slugify}", |
||||
|
"${slugify(event.user.name)}", |
||||
|
"{{event.user.name | slugify}}" |
||||
|
)] |
||||
|
public async Task Should_transform_slugify(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent { User = user }; |
||||
|
|
||||
|
A.CallTo(() => user.Claims) |
||||
|
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck") }); |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("donald-duck", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"${USER_NAME | Upper | Trim}", |
||||
|
"${EVENT_USER.NAME | Upper | Trim}", |
||||
|
"${event.user.name.toUpperCase().trim()}", |
||||
|
"{{event.user.name | upcase | trim}}" |
||||
|
)] |
||||
|
public async Task Should_transform_chained(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent { User = user }; |
||||
|
|
||||
|
A.CallTo(() => user.Claims) |
||||
|
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald Duck ") }); |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("DONALD DUCK", result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[Expressions( |
||||
|
"${USER_NAME | Escape}", |
||||
|
"${EVENT_USER.NAME | Escape}", |
||||
|
null, |
||||
|
"{{event.user.name | escape}}" |
||||
|
)] |
||||
|
public async Task Should_transform_json_escape(string script) |
||||
|
{ |
||||
|
var @event = new EnrichedContentEvent { User = user }; |
||||
|
|
||||
|
A.CallTo(() => user.Claims) |
||||
|
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "Donald\"Duck") }); |
||||
|
|
||||
|
var result = await sut.FormatAsync(script, @event); |
||||
|
|
||||
|
Assert.Equal("Donald\\\"Duck", result); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,129 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Core.Templates; |
||||
|
using Squidex.Domain.Apps.Core.Templates.Extensions; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Operations.Templates |
||||
|
{ |
||||
|
public class FluidTemplateEngineTests |
||||
|
{ |
||||
|
private readonly FluidTemplateEngine sut; |
||||
|
|
||||
|
public FluidTemplateEngineTests() |
||||
|
{ |
||||
|
var extensions = new IFluidExtension[] |
||||
|
{ |
||||
|
new DateTimeFluidExtension() |
||||
|
}; |
||||
|
|
||||
|
sut = new FluidTemplateEngine(extensions); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData("{{ e.user }}", "subject:me")] |
||||
|
[InlineData("{{ e.user.type }}", "subject")] |
||||
|
[InlineData("{{ e.user.identifier }}", "me")] |
||||
|
public async Task Should_render_ref_token(string template, string expected) |
||||
|
{ |
||||
|
var value = new |
||||
|
{ |
||||
|
User = new RefToken(RefTokenType.Subject, "me") |
||||
|
}; |
||||
|
|
||||
|
var result = await RenderAync(template, value); |
||||
|
|
||||
|
Assert.Equal(expected, result); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData("{{ e.id }}", "42,my-app")] |
||||
|
[InlineData("{{ e.id.name}}", "my-app")] |
||||
|
[InlineData("{{ e.id.id }}", "42")] |
||||
|
public async Task Should_render_named_id(string template, string expected) |
||||
|
{ |
||||
|
var value = new |
||||
|
{ |
||||
|
Id = NamedId.Of("42", "my-app") |
||||
|
}; |
||||
|
|
||||
|
var result = await RenderAync(template, value); |
||||
|
|
||||
|
Assert.Equal(expected, result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_format_enum() |
||||
|
{ |
||||
|
var value = new |
||||
|
{ |
||||
|
Type = EnrichedContentEventType.Created |
||||
|
}; |
||||
|
|
||||
|
var template = "{{ e.type }}"; |
||||
|
|
||||
|
var result = await RenderAync(template, value); |
||||
|
|
||||
|
Assert.Equal(value.Type.ToString(), result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_format_date() |
||||
|
{ |
||||
|
var now = DateTime.UtcNow; |
||||
|
|
||||
|
var value = new |
||||
|
{ |
||||
|
Timestamp = now |
||||
|
}; |
||||
|
|
||||
|
var template = "{{ e.timestamp | format_date: 'yyyy-MM-dd-hh-mm-ss' }}"; |
||||
|
|
||||
|
var result = await RenderAync(template, value); |
||||
|
|
||||
|
Assert.Equal($"{now:yyyy-MM-dd-hh-mm-ss}", result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_format_content_data() |
||||
|
{ |
||||
|
var template = "{{ e.data.value.en }}"; |
||||
|
|
||||
|
var value = new |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("value", |
||||
|
new ContentFieldData() |
||||
|
.AddValue("en", "Hello")) |
||||
|
}; |
||||
|
|
||||
|
var result = await RenderAync(template, value); |
||||
|
|
||||
|
Assert.Equal("Hello", result); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_throw_exception_when_template_invalid() |
||||
|
{ |
||||
|
var template = "{% for x of event %}"; |
||||
|
|
||||
|
await Assert.ThrowsAsync<TemplateParseException>(() => sut.RenderAsync(template, new TemplateVars())); |
||||
|
} |
||||
|
|
||||
|
private Task<string> RenderAync(string template, object value) |
||||
|
{ |
||||
|
return sut.RenderAsync(template, new TemplateVars { ["e"] = value }); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,104 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
||||
|
using Squidex.Domain.Apps.Core.Templates; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public class ReferenceFluidExtensionTests |
||||
|
{ |
||||
|
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); |
||||
|
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); |
||||
|
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); |
||||
|
private readonly FluidTemplateEngine sut; |
||||
|
|
||||
|
public ReferenceFluidExtensionTests() |
||||
|
{ |
||||
|
var extensions = new IFluidExtension[] |
||||
|
{ |
||||
|
new ReferencesFluidExtension(contentQuery, appProvider) |
||||
|
}; |
||||
|
|
||||
|
A.CallTo(() => appProvider.GetAppAsync(appId.Id)) |
||||
|
.Returns(Mocks.App(appId)); |
||||
|
|
||||
|
sut = new FluidTemplateEngine(extensions); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_resolve_references_in_loop() |
||||
|
{ |
||||
|
var referenceId1 = Guid.NewGuid(); |
||||
|
var reference1 = CreateReference(referenceId1, 1); |
||||
|
var referenceId2 = Guid.NewGuid(); |
||||
|
var reference2 = CreateReference(referenceId1, 2); |
||||
|
|
||||
|
var @event = new EnrichedContentEvent |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("references", |
||||
|
new ContentFieldData() |
||||
|
.AddJsonValue(JsonValue.Array(referenceId1, referenceId2))), |
||||
|
AppId = appId |
||||
|
}; |
||||
|
|
||||
|
A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<IReadOnlyList<Guid>>.That.Contains(referenceId1))) |
||||
|
.Returns(ResultList.CreateFrom(1, reference1)); |
||||
|
|
||||
|
A.CallTo(() => contentQuery.QueryAsync(A<Context>._, A<IReadOnlyList<Guid>>.That.Contains(referenceId2))) |
||||
|
.Returns(ResultList.CreateFrom(1, reference2)); |
||||
|
|
||||
|
var vars = new TemplateVars |
||||
|
{ |
||||
|
["event"] = @event |
||||
|
}; |
||||
|
|
||||
|
var template = @"
|
||||
|
{% for id in event.data.references.iv %} |
||||
|
{% reference 'ref', id %} |
||||
|
Text: {{ ref.data.field1.iv }} {{ ref.data.field2.iv }} |
||||
|
{% endfor %} |
||||
|
";
|
||||
|
|
||||
|
var expected = @"
|
||||
|
Text: Hello 1 World 1 |
||||
|
Text: Hello 2 World 2 |
||||
|
";
|
||||
|
|
||||
|
var result = await sut.RenderAsync(template, vars); |
||||
|
|
||||
|
Assert.Equal(expected, result); |
||||
|
} |
||||
|
|
||||
|
private IEnrichedContentEntity CreateReference(Guid referenceId, int index) |
||||
|
{ |
||||
|
return new ContentEntity |
||||
|
{ |
||||
|
Data = |
||||
|
new NamedContentData() |
||||
|
.AddField("field1", |
||||
|
new ContentFieldData() |
||||
|
.AddJsonValue(JsonValue.Create($"Hello {index}"))) |
||||
|
.AddField("field2", |
||||
|
new ContentFieldData() |
||||
|
.AddJsonValue(JsonValue.Create($"World {index}"))), |
||||
|
Id = referenceId |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue