diff --git a/src/Take.Blip.Builder.UnitTests/Actions/ExecuteTemplateActionTests.cs b/src/Take.Blip.Builder.UnitTests/Actions/ExecuteTemplateActionTests.cs new file mode 100644 index 00000000..adac8a4b --- /dev/null +++ b/src/Take.Blip.Builder.UnitTests/Actions/ExecuteTemplateActionTests.cs @@ -0,0 +1,170 @@ +using System; +using System.Threading.Tasks; +using HandlebarsDotNet; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Serilog; +using Take.Blip.Builder.Actions.ExecuteTemplate; +using Xunit; +using Shouldly; + +namespace Take.Blip.Builder.UnitTests.Actions +{ + public class ExecuteTemplateActionTests : ActionTestsBase + { + private IHandlebars Handlebars = Substitute.For(); + + private ExecuteTemplateAction GetTarget() + { + return new ExecuteTemplateAction(Handlebars,Substitute.For()); + } + + [Fact] + public async Task ExecuteTemplateShouldSuccess() + { + //Arrange + var variableName = "TestName"; + var outputVariable = ""; + Context.GetVariableAsync(nameof(variableName), CancellationToken).Returns(variableName); + + var templateResult = "TemplateResult"; + var handlebarsTemplate = Substitute.For>(); + handlebarsTemplate(Arg.Any()).Returns("TemplateResult"); + Handlebars.Compile(Arg.Any()).Returns(handlebarsTemplate); + + var settings = new ExecuteTemplateSettings + { + InputVariables = new []{ nameof(variableName) }, + Template = $"Name: {{{{{nameof(variableName)}}}}}", + OutputVariable = outputVariable + }; + + //Act + var action = GetTarget(); + await action.ExecuteAsync(Context, settings, CancellationToken); + + // Assert + Handlebars.Received(1).Compile(settings.Template); + await Context.Received(1).SetVariableAsync(Arg.Any(), Arg.Is(templateResult), CancellationToken, Arg.Any()); + } + + [Fact] + public async Task ExecuteTemplateWithObjectAsPropertyShouldSuccess() + { + //Arrange + var variableObj = "{ \"people\": [{\"name\": \"TestName\", \"city\": \"Aracaju\"}, {\"name\": \"TestName2\", \"city\": \"Bahia\"}] }"; + var outputVariable = ""; + Context.GetVariableAsync(nameof(variableObj), CancellationToken).Returns(variableObj); + + var templateResult = "TemplateResult"; + var handlebarsTemplate = Substitute.For>(); + handlebarsTemplate(Arg.Any()).Returns("TemplateResult"); + Handlebars.Compile(Arg.Any()).Returns(handlebarsTemplate); + + var settings = new ExecuteTemplateSettings + { + InputVariables = new []{ nameof(variableObj) }, + Template = "Names: {{#each people}}{{name}} living in {{city}} {{/each}}", + OutputVariable = outputVariable + }; + + //Act + var action = GetTarget(); + await action.ExecuteAsync(Context, settings, CancellationToken); + + // Assert + Handlebars.Received(1).Compile(settings.Template); + await Context.Received(1).SetVariableAsync(Arg.Any(), Arg.Is(templateResult), CancellationToken, Arg.Any()); + } + + [Fact] + public async Task ExecuteTemplateWithObjectAndStringVariablesAsPropertyShouldSuccess() + { + //Arrange + var variableName = "Peoples:"; + var variableObj = "{ \"people\": [{\"name\": \"TestName\", \"city\": \"Aracaju\"}, {\"name\": \"TestName2\", \"city\": \"Bahia\"}] }"; + var outputVariable = ""; + Context.GetVariableAsync(nameof(variableName), CancellationToken).Returns(variableName); + Context.GetVariableAsync(nameof(variableObj), CancellationToken).Returns(variableObj); + + var templateResult = "TemplateResult"; + var handlebarsTemplate = Substitute.For>(); + handlebarsTemplate(Arg.Any()).Returns("TemplateResult"); + Handlebars.Compile(Arg.Any()).Returns(handlebarsTemplate); + var settings = new ExecuteTemplateSettings + { + InputVariables = new []{ nameof(variableName), nameof(variableObj) }, + Template = $"{{{{{nameof(variableName)}}}}} {{{{#each people}}}}{{{{name}}}} living in {{{{city}}}} {{{{/each}}}}", + OutputVariable = outputVariable + }; + + //Act + var action = GetTarget(); + await action.ExecuteAsync(Context, settings, CancellationToken); + + // Assert + Handlebars.Received(1).Compile(settings.Template); + await Context.Received(1).SetVariableAsync(Arg.Any(), Arg.Is(templateResult), CancellationToken, Arg.Any()); + } + + [Fact] + public async Task ExecuteTemplateErrorHandlebarsParseShouldFail() + { + //Arrange + var variableName = "TestName"; + var outputVariable = ""; + Context.GetVariableAsync(nameof(variableName), CancellationToken).Returns(variableName); + Handlebars.Compile("").ThrowsForAnyArgs(new HandlebarsParserException("could not be converted to an expression")); + var settings = new ExecuteTemplateSettings + { + InputVariables = new []{ nameof(variableName) }, + Template = $"Name: {{{{nameof(variableName)}}}}", + OutputVariable = outputVariable + }; + + //Act + var action = GetTarget(); + try + { + await action.ExecuteAsync(Context, settings, CancellationToken); + } + catch (HandlebarsParserException ex) + { + ex.Message.ShouldContain("could not be converted to an expression"); + } + + Handlebars.Received(1).Compile(settings.Template); + await Context.Received(0).SetVariableAsync(Arg.Any(), Arg.Any(), CancellationToken, Arg.Any()); + } + + [Fact] + public async Task ExecuteTemplateErrorHandlebarsExecutionShouldFail() + { + //Arrange + var variableName = "TestName"; + var outputVariable = ""; + Context.GetVariableAsync(nameof(variableName), CancellationToken).Returns(variableName); + Handlebars.Compile("").ThrowsForAnyArgs(new Exception("Error executing the template")); + var settings = new ExecuteTemplateSettings + { + InputVariables = new []{ nameof(variableName) }, + Template = $"Name: {{{{nameof(variableName)}}}}", + OutputVariable = outputVariable + }; + + //Act + var action = GetTarget(); + try + { + await action.ExecuteAsync(Context, settings, CancellationToken); + } + catch (Exception ex) + { + ex.Message.ShouldContain("Error executing the template"); + } + + Handlebars.Received(1).Compile(settings.Template); + await Context.Received(0).SetVariableAsync(Arg.Any(), Arg.Any(), CancellationToken, Arg.Any()); + } + } +} \ No newline at end of file diff --git a/src/Take.Blip.Builder/Actions/ExecuteTemplate/ExecuteTemplateAction.cs b/src/Take.Blip.Builder/Actions/ExecuteTemplate/ExecuteTemplateAction.cs new file mode 100644 index 00000000..80641784 --- /dev/null +++ b/src/Take.Blip.Builder/Actions/ExecuteTemplate/ExecuteTemplateAction.cs @@ -0,0 +1,103 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Esprima; +using HandlebarsDotNet; +using Lime.Protocol; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Serilog; +using Take.Blip.Builder.Utils; +using Takenet.Iris.Messaging; + +namespace Take.Blip.Builder.Actions.ExecuteTemplate +{ + public class ExecuteTemplateAction : ActionBase + { + private readonly ILogger _logger; + private readonly IHandlebars _handlebars; + + public ExecuteTemplateAction(IHandlebars handlebars, ILogger logger) : base(nameof(ExecuteTemplateAction)) + { + _logger = logger; + _handlebars = handlebars; + } + + public override async Task ExecuteAsync(IContext context, ExecuteTemplateSettings settings, CancellationToken cancellationToken) + { + var result = string.Empty; + try + { + var arguments = await GetScriptArgumentsAsync(context, settings, cancellationToken); + var obj = CopyArgumentsToObject(arguments); + var template = _handlebars.Compile(settings.Template); + result = template(obj); + } + catch (Exception ex) + { + if (ex is HandlebarsParserException) + { + _logger.Warning(ex, "Unexpected error while trying to parse Handlebars template"); + throw; + } + + _logger.Warning(ex, "Unexpected error while execute action Execute Template"); + throw; + } + + await SetScriptResultAsync(context, settings, result, cancellationToken); + } + + private async Task GetScriptArgumentsAsync( + IContext context, ExecuteTemplateSettings settings, CancellationToken cancellationToken) + { + var obj = new JObject(); + + if (settings.InputVariables.IsNullOrEmpty()) + { + return obj; + } + + foreach (var variable in settings.InputVariables) + { + var variableValue = await context.GetVariableAsync(variable, cancellationToken); + obj[variable] = variableValue; + } + return obj; + } + + private async Task SetScriptResultAsync( + IContext context, ExecuteTemplateSettings settings, string result, CancellationToken cancellationToken) + { + if (result.IsNullOrEmpty()) + { + await context.DeleteVariableAsync(settings.OutputVariable, cancellationToken); + } + else + { + await context.SetVariableAsync(settings.OutputVariable, result, cancellationToken); + } + } + + private JObject CopyArgumentsToObject(JObject arguments) + { + var obj = new JObject(); + foreach (var property in arguments.Properties()) + { + try + { + var deserializedJson = JsonConvert.DeserializeObject(property.Value.ToString(), JsonSerializerSettingsContainer.Settings); + if (deserializedJson != null) + { + obj.Merge(deserializedJson); + } + } catch (JsonReaderException) + { + obj[property.Name] = property.Value; + } + } + + return obj; + } + } +} \ No newline at end of file diff --git a/src/Take.Blip.Builder/Actions/ExecuteTemplate/ExecuteTemplateSettings.cs b/src/Take.Blip.Builder/Actions/ExecuteTemplate/ExecuteTemplateSettings.cs new file mode 100644 index 00000000..7380518a --- /dev/null +++ b/src/Take.Blip.Builder/Actions/ExecuteTemplate/ExecuteTemplateSettings.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using Take.Blip.Builder.Models; + +namespace Take.Blip.Builder.Actions.ExecuteTemplate +{ + /// + /// Settings to Execute Template Action + /// + public class ExecuteTemplateSettings : IValidable + { + /// + /// Input Variables + /// + public string[] InputVariables { get; set; } + + /// + /// Output Variable + /// + public string OutputVariable { get; set; } + + /// + /// Template that will be transformed + /// + public string Template { get; set; } + + public void Validate() + { + if (string.IsNullOrEmpty(OutputVariable)) + { + throw new ValidationException($"The '{nameof(OutputVariable)}' settings value is required for '{nameof(ExecuteTemplateSettings)}' action"); + } + } + } +} \ No newline at end of file diff --git a/src/Take.Blip.Builder/Hosting/ContainerExtensions.cs b/src/Take.Blip.Builder/Hosting/ContainerExtensions.cs index 17d401b1..41bae3fc 100644 --- a/src/Take.Blip.Builder/Hosting/ContainerExtensions.cs +++ b/src/Take.Blip.Builder/Hosting/ContainerExtensions.cs @@ -7,6 +7,7 @@ using Take.Blip.Builder.Actions.CreateTicket; using Take.Blip.Builder.Actions.DeleteVariable; using Take.Blip.Builder.Actions.ExecuteScript; +using Take.Blip.Builder.Actions.ExecuteTemplate; using Take.Blip.Builder.Actions.ManageList; using Take.Blip.Builder.Actions.MergeContact; using Take.Blip.Builder.Actions.ProcessCommand; @@ -95,7 +96,8 @@ private static Container RegisterBuilderActions(this Container container) typeof(CreateTicketAction), typeof(DeleteVariableAction), typeof(ProcessContentAssistantAction), - typeof(TrackContactsJourneyAction) + typeof(TrackContactsJourneyAction), + typeof(ExecuteTemplateAction) }); return container; diff --git a/src/Take.Blip.Builder/Take.Blip.Builder.csproj b/src/Take.Blip.Builder/Take.Blip.Builder.csproj index 73e5c64b..69ec76ad 100644 --- a/src/Take.Blip.Builder/Take.Blip.Builder.csproj +++ b/src/Take.Blip.Builder/Take.Blip.Builder.csproj @@ -8,12 +8,14 @@ + +