Skip to content

Commit

Permalink
feat: extend data files (#59)
Browse files Browse the repository at this point in the history
+semver: minor
  • Loading branch information
ewingjm authored Jan 15, 2021
1 parent 1753cd0 commit 771923b
Show file tree
Hide file tree
Showing 13 changed files with 238 additions and 108 deletions.
44 changes: 25 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,22 @@ Given I have created 'a record'
```
or
```gherkin
Given 'someone' has created 'a record'
Given 'someone' has created 'a record with a difference'
```

The examples above will both look for a JSON file named _a record.json_ in the _data_ folder (you must ensure that these files are copying to the build output directory). The difference is that the latter requires the following in the configuration file:
These bindings look for a corresponding JSON file in a _data_ folder in the root of your project. The file is resolved using a combination of the directory structure and the file name. For example, the bindings above could resolve the following files:

- a user with an alias of _someone_ in the `users` array
```
└───data
│ a record.json
└───a record
with a difference.json
```

If you are using the binding which creates data as someone other than the current user, you will need the following configuration to be present:

- a user with a matching alias in the `users` array that has the `username` set
- an application user with sufficient privileges to impersonate the above user configured in the `applicationUser` property.

Refer to the Microsoft documentation on creating an application user [here](https://docs.microsoft.com/en-us/power-platform/admin/create-users-assign-online-security-roles#create-an-application-user).
Expand Down Expand Up @@ -153,25 +163,17 @@ The example above will create the following:
- An opportunity related to the account
- A task related to the opportunity

The `@logicalName` property is required for the root record.
In addition to the standard Web API syntax, we also have the following:

The `@alias` property can optionally be added to any record and allows the record to be referenced in certain bindings. The _Given I have created_ binding itself supports relating records using `@alias.bind` syntax, as shown below:

```json
{
"@logicalName": "account",
"@alias": "sample account",
"name": "Sample Account",
"[email protected]": "sample contact"
}
```
| Property | Description | Requirement |
|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------|
| @logicalName | the entity logical name of the root record | Mandatory (unless included using `@extends` - see below) |
| @alias | a friendly alias that can be used to reference the created record in certain bindings. Can be set on nested records | Optional |
| @extends | a relative path to a data file to extend. Records in arrays are merged by index (you may need to include blank objects to insert new records into the array) | Optional |

#### Dynamic data

We also support the use of
[faker.js](https://github.com/marak/Faker.js) moustache template syntax for generating dynamic test data at run-time. Please refer to the faker documentation for all of the functionality that is available.

The below JSON will generate a contact with a random name, credit limit, email address, and date of birth in the past 90 years:
We support [faker.js](https://github.com/marak/Faker.js) moustache template syntax for generating dynamic test data at run-time. Please refer to the faker documentation for all of the functionality that is available. The below JSON will generate a contact with a random name, credit limit, email address, and date of birth in the past 90 years:

```json
{
Expand All @@ -181,12 +183,16 @@ The below JSON will generate a contact with a random name, credit limit, email a
"firstname": "{{name.lastName}}",
"[email protected]": "{{finance.amount}}",
"emailaddress1": "{{internet.email}}",
"[email protected]": "{{date.past(90)}}"
"[email protected]": "{{date.past(90)}}",
"[email protected]": "sample account"
}
```

When using faker syntax, you must also annotate number or date fields using `@faker.number`, `@faker.date` or `@faker.dateonly` to ensure that the JSON is formatted correctly.

You can also dynamically set lookups by alias using `<lookup>@alias.bind` (this is limited to aliased records in other files - not the current file).


## Contributing

Please refer to the [Contributing](./CONTRIBUTING.md) guide.
Expand Down
4 changes: 4 additions & 0 deletions bindings/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[*.cs]

# SA1633: File should have header
dotnet_diagnostic.SA1633.severity = none
8 changes: 8 additions & 0 deletions bindings/Capgemini.PowerApps.SpecFlowBindings.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ VisualStudioVersion = 16.0.29230.47
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4C52F806-6E08-48B3-BC9C-0054D7B8FCA4}"
ProjectSection(SolutionItems) = preProject
..\.editorconfig = ..\.editorconfig
NuGet.config = NuGet.config
..\README.md = ..\README.md
EndProjectSection
Expand All @@ -17,6 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{8F8FAAAA
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Capgemini.PowerApps.SpecFlowBindings.UiTests", "tests\Capgemini.PowerApps.SpecFlowBindings.UiTests\Capgemini.PowerApps.SpecFlowBindings.UiTests.csproj", "{EFDA0239-5116-4DFA-90AA-644DD7509017}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Capgemini.PowerApps.SpecFlowBindings.MSBuild", "src\Capgemini.PowerApps.SpecFlowBindings.MSBuild\Capgemini.PowerApps.SpecFlowBindings.MSBuild.csproj", "{7D743F20-F84A-4719-B4CA-5A9FDF895573}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -31,13 +34,18 @@ Global
{EFDA0239-5116-4DFA-90AA-644DD7509017}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EFDA0239-5116-4DFA-90AA-644DD7509017}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EFDA0239-5116-4DFA-90AA-644DD7509017}.Release|Any CPU.Build.0 = Release|Any CPU
{7D743F20-F84A-4719-B4CA-5A9FDF895573}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D743F20-F84A-4719-B4CA-5A9FDF895573}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D743F20-F84A-4719-B4CA-5A9FDF895573}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D743F20-F84A-4719-B4CA-5A9FDF895573}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{88DD21C5-5EE8-4023-847E-2A87E434AD22} = {1BA90815-A570-44B9-96C8-1D087FF8A060}
{EFDA0239-5116-4DFA-90AA-644DD7509017} = {8F8FAAAA-5C66-4ECC-BC4A-1127C1E7FFC8}
{7D743F20-F84A-4719-B4CA-5A9FDF895573} = {1BA90815-A570-44B9-96C8-1D087FF8A060}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8038BFA0-6A34-41DE-992A-1B4228585614}
Expand Down
6 changes: 0 additions & 6 deletions bindings/NuGet.config

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="16.8.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
namespace Capgemini.PowerApps.SpecFlowBindings.MSBuild
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Newtonsoft.Json.Linq;

/// <summary>
/// Extend JSON data files using the "@extends" property.
/// </summary>
public class ExtendDataFiles : Task
{
private const string ExtendsProperty = "@extends";

private JsonMergeSettings jsonMergeSettings;

/// <summary>
/// Gets or sets the items to be extended.
/// </summary>
/// <value>
/// The items to be extended.
/// </value>
[Required]
public ITaskItem[] Include { get; set; }

/// <summary>
/// Gets or sets the path to output compiled data files to.
/// </summary>
/// <value>
/// The path to output compiled data files to.
/// </value>
[Required]
public ITaskItem DestinationFolder { get; set; }

/// <summary>
/// Gets or sets a value indicating how to handle the merging of arrays <see cref="MergeArrayHandling"/>.
/// </summary>
public string ArrayHandling { get; set; }

/// <summary>
/// Gets the parsed <see cref="MergeArrayHandling"/> value to use.
/// </summary>
/// <value>
/// The parsed <see cref="MergeArrayHandling"/> value to use.
/// </value>
protected MergeArrayHandling MergeArrayHandling => !string.IsNullOrEmpty(this.ArrayHandling) ? (MergeArrayHandling)Enum.Parse(typeof(MergeArrayHandling), this.ArrayHandling) : MergeArrayHandling.Merge;

private JsonMergeSettings JsonMergeSettings
{
get
{
if (this.jsonMergeSettings == null)
{
this.jsonMergeSettings = new JsonMergeSettings
{
MergeArrayHandling = this.MergeArrayHandling,
MergeNullValueHandling = MergeNullValueHandling.Merge,
PropertyNameComparison = StringComparison.InvariantCultureIgnoreCase,
};
}

return this.jsonMergeSettings;
}
}

/// <inheritdoc/>
public override bool Execute()
{
var succeeded = true;

Directory.CreateDirectory(this.DestinationFolder.ItemSpec);

foreach (var taskItem in this.Include)
{
this.CompileDataFile(taskItem.ItemSpec);
}

return succeeded;
}

private void CompileDataFile(string itemPath)
{
this.Log.LogMessage(MessageImportance.Normal, $"Processing data file at '{itemPath}'.");

var mergeStack = this.GetMergeStack(itemPath);
var rootJson = mergeStack.Pop();
while (mergeStack.Count > 0)
{
rootJson.Merge(mergeStack.Pop(), this.JsonMergeSettings);
}

rootJson.Property(ExtendsProperty)?.Remove();

File.WriteAllText(this.GetFileOutputPath(itemPath), rootJson.ToString());
}

private string GetFileOutputPath(string itemPath)
{
return Path.Combine(this.DestinationFolder.ItemSpec, string.Join(" ", itemPath.Split('\\').Skip(1)));
}

private Stack<JObject> GetMergeStack(string itemPath, Stack<JObject> existingStack = null)
{
var stack = existingStack ?? new Stack<JObject>();
var data = JObject.Parse(File.ReadAllText(itemPath));
stack.Push(data);

var extends = data[ExtendsProperty]?.ToString();
if (!string.IsNullOrEmpty(extends))
{
this.Log.LogMessage(MessageImportance.Low, $"Adding {extends} to merge stack.");
this.GetMergeStack(
Path.Combine(Path.GetDirectoryName(itemPath), $"{extends}.json"),
stack);
}

return stack;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,43 @@
<Copyright>Capgemini Copyright © 2020</Copyright>
<Version>0.1.0</Version>
<PackageIcon>icon.png</PackageIcon>
<CodeAnalysisRuleSet>Capgemini.PowerApps.SpecFlowBindings.ruleset</CodeAnalysisRuleSet>
<Authors>Capgemini_UK, ewingjm</Authors>
<PackageId>Capgemini.PowerApps.SpecFlowBindings</PackageId>
<PackageProjectUrl>https://github.com/Capgemini/powerapps-specflow-bindings</PackageProjectUrl>
<PackageTags>powerapps specflow bindings automated-testing testing</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<DebugType>portable</DebugType>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<PropertyGroup>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

<Target Name="CopyDriver" AfterTargets="Build">
<Exec Command="npm run build" WorkingDirectory="../../../driver" />
<Copy SourceFiles="../../../driver/dist/driver.js" DestinationFolder="bin/lib" />
</Target>

<Target Name="CopyTaskAssembly" AfterTargets="Build">
<MSBuild Projects="../Capgemini.PowerApps.SpecFlowBindings.MSBuild/Capgemini.PowerApps.SpecFlowBindings.MSBuild.csproj">
<Output TaskParameter="TargetOutputs" ItemName="BuildTaskAssemblyOutputs" />
</MSBuild>
<PropertyGroup>
<_OutputDirectory>@(BuildTaskAssemblyOutputs->'%(RelativeDir)')</_OutputDirectory>
</PropertyGroup>
<ItemGroup>
<Content Include="$(_OutputDirectory)**\*">
<Pack>true</Pack>
<PackagePath>build/Capgemini.PowerApps.SpecFlowBindings.MSBuild</PackagePath>
</Content>
</ItemGroup>
</Target>

<ItemGroup>
<PackageReference Include="Dynamics365.UIAutomation.Api" Version="9.1.0.27021" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
Expand Down Expand Up @@ -53,6 +71,7 @@
</PackageReference>
<PackageReference Include="YamlDotNet" Version="9.1.0" />
</ItemGroup>

<ItemGroup>
<Content Include="build\**">
<Pack>true</Pack>
Expand All @@ -66,7 +85,6 @@
<Pack>true</Pack>
<PackagePath>content</PackagePath>
</Content>
<None Include="Capgemini.PowerApps.SpecFlowBindings.ruleset" />
<None Include="icon.png" Pack="true" PackagePath="\" />
<!-- Including driver files so that they are picked up by SonarCloud analysis (see https://sonarcloud.io/documentation/analysis/scan/sonarscanner-for-msbuild) -->
<None Include="..\..\..\driver\**\*.ts" Exclude="..\..\..\driver\node_modules\**\*" Visible="false" />
Expand Down
Loading

0 comments on commit 771923b

Please sign in to comment.