From 771923bab7ca8e4637c2e11e2583194bbda97a27 Mon Sep 17 00:00:00 2001 From: Max Ewing Date: Fri, 15 Jan 2021 20:43:20 +0000 Subject: [PATCH] feat: extend data files (#59) +semver: minor --- README.md | 44 ++++--- bindings/.editorconfig | 4 + .../Capgemini.PowerApps.SpecFlowBindings.sln | 8 ++ bindings/NuGet.config | 6 - ....PowerApps.SpecFlowBindings.MSBuild.csproj | 21 +++ .../ExtendDataFiles.cs | 123 ++++++++++++++++++ ...apgemini.PowerApps.SpecFlowBindings.csproj | 22 +++- ...pgemini.PowerApps.SpecFlowBindings.ruleset | 75 ----------- ...pgemini.PowerApps.SpecFlowBindings.targets | 8 ++ ....PowerApps.SpecFlowBindings.UiTests.csproj | 24 +++- .../that has been extended with @extend.json | 4 + .../DataSteps.feature | 5 +- templates/include-build-and-test-steps.yml | 2 +- 13 files changed, 238 insertions(+), 108 deletions(-) create mode 100644 bindings/.editorconfig delete mode 100644 bindings/NuGet.config create mode 100644 bindings/src/Capgemini.PowerApps.SpecFlowBindings.MSBuild/Capgemini.PowerApps.SpecFlowBindings.MSBuild.csproj create mode 100644 bindings/src/Capgemini.PowerApps.SpecFlowBindings.MSBuild/ExtendDataFiles.cs delete mode 100644 bindings/src/Capgemini.PowerApps.SpecFlowBindings/Capgemini.PowerApps.SpecFlowBindings.ruleset create mode 100644 bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/Data/a contact/that has been extended with @extend.json diff --git a/README.md b/README.md index e8517dd..80e7a0f 100644 --- a/README.md +++ b/README.md @@ -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). @@ -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", - "primarycontactid@alias.bind": "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 { @@ -181,12 +183,16 @@ The below JSON will generate a contact with a random name, credit limit, email a "firstname": "{{name.lastName}}", "creditlimit@faker.number": "{{finance.amount}}", "emailaddress1": "{{internet.email}}", - "birthdate@faker.date": "{{date.past(90)}}" + "birthdate@faker.date": "{{date.past(90)}}", + "accountid@alias.bind": "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 `@alias.bind` (this is limited to aliased records in other files - not the current file). + + ## Contributing Please refer to the [Contributing](./CONTRIBUTING.md) guide. diff --git a/bindings/.editorconfig b/bindings/.editorconfig new file mode 100644 index 0000000..5ccde79 --- /dev/null +++ b/bindings/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# SA1633: File should have header +dotnet_diagnostic.SA1633.severity = none diff --git a/bindings/Capgemini.PowerApps.SpecFlowBindings.sln b/bindings/Capgemini.PowerApps.SpecFlowBindings.sln index 8da16de..d218d80 100644 --- a/bindings/Capgemini.PowerApps.SpecFlowBindings.sln +++ b/bindings/Capgemini.PowerApps.SpecFlowBindings.sln @@ -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 @@ -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 @@ -31,6 +34,10 @@ 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 @@ -38,6 +45,7 @@ Global 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} diff --git a/bindings/NuGet.config b/bindings/NuGet.config deleted file mode 100644 index 724f227..0000000 --- a/bindings/NuGet.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/bindings/src/Capgemini.PowerApps.SpecFlowBindings.MSBuild/Capgemini.PowerApps.SpecFlowBindings.MSBuild.csproj b/bindings/src/Capgemini.PowerApps.SpecFlowBindings.MSBuild/Capgemini.PowerApps.SpecFlowBindings.MSBuild.csproj new file mode 100644 index 0000000..43d6519 --- /dev/null +++ b/bindings/src/Capgemini.PowerApps.SpecFlowBindings.MSBuild/Capgemini.PowerApps.SpecFlowBindings.MSBuild.csproj @@ -0,0 +1,21 @@ + + + + netstandard2.0 + true + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/bindings/src/Capgemini.PowerApps.SpecFlowBindings.MSBuild/ExtendDataFiles.cs b/bindings/src/Capgemini.PowerApps.SpecFlowBindings.MSBuild/ExtendDataFiles.cs new file mode 100644 index 0000000..fcc384b --- /dev/null +++ b/bindings/src/Capgemini.PowerApps.SpecFlowBindings.MSBuild/ExtendDataFiles.cs @@ -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; + + /// + /// Extend JSON data files using the "@extends" property. + /// + public class ExtendDataFiles : Task + { + private const string ExtendsProperty = "@extends"; + + private JsonMergeSettings jsonMergeSettings; + + /// + /// Gets or sets the items to be extended. + /// + /// + /// The items to be extended. + /// + [Required] + public ITaskItem[] Include { get; set; } + + /// + /// Gets or sets the path to output compiled data files to. + /// + /// + /// The path to output compiled data files to. + /// + [Required] + public ITaskItem DestinationFolder { get; set; } + + /// + /// Gets or sets a value indicating how to handle the merging of arrays . + /// + public string ArrayHandling { get; set; } + + /// + /// Gets the parsed value to use. + /// + /// + /// The parsed value to use. + /// + 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; + } + } + + /// + 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 GetMergeStack(string itemPath, Stack existingStack = null) + { + var stack = existingStack ?? new Stack(); + 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; + } + } +} diff --git a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Capgemini.PowerApps.SpecFlowBindings.csproj b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Capgemini.PowerApps.SpecFlowBindings.csproj index d353c6d..d0bb25f 100644 --- a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Capgemini.PowerApps.SpecFlowBindings.csproj +++ b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Capgemini.PowerApps.SpecFlowBindings.csproj @@ -7,7 +7,6 @@ Capgemini Copyright © 2020 0.1.0 icon.png - Capgemini.PowerApps.SpecFlowBindings.ruleset Capgemini_UK, ewingjm Capgemini.PowerApps.SpecFlowBindings https://github.com/Capgemini/powerapps-specflow-bindings @@ -15,17 +14,36 @@ MIT true portable + true + true true true snupkg + + + + + + + + <_OutputDirectory>@(BuildTaskAssemblyOutputs->'%(RelativeDir)') + + + + true + build/Capgemini.PowerApps.SpecFlowBindings.MSBuild + + + + @@ -53,6 +71,7 @@ + true @@ -66,7 +85,6 @@ true content - diff --git a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Capgemini.PowerApps.SpecFlowBindings.ruleset b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Capgemini.PowerApps.SpecFlowBindings.ruleset deleted file mode 100644 index 21992e0..0000000 --- a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Capgemini.PowerApps.SpecFlowBindings.ruleset +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/build/Capgemini.PowerApps.SpecFlowBindings.targets b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/build/Capgemini.PowerApps.SpecFlowBindings.targets index c73efb4..f992083 100644 --- a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/build/Capgemini.PowerApps.SpecFlowBindings.targets +++ b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/build/Capgemini.PowerApps.SpecFlowBindings.targets @@ -11,4 +11,12 @@ false + + + + + + + + diff --git a/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/Capgemini.PowerApps.SpecFlowBindings.UiTests.csproj b/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/Capgemini.PowerApps.SpecFlowBindings.UiTests.csproj index 213e115..d159135 100644 --- a/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/Capgemini.PowerApps.SpecFlowBindings.UiTests.csproj +++ b/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/Capgemini.PowerApps.SpecFlowBindings.UiTests.csproj @@ -14,6 +14,26 @@ + + + + + + <_MSBuildOutputPath>@(_MSBuildTaskProjectOutput -> '%(RelativeDir)') + + + <_MSBuildOutputFiles Include="$(_MSBuildOutputPath)/**/*" Visible="false" /> + + + + + + + + + + + @@ -27,10 +47,6 @@ PreserveNewest - - PreserveNewest - - diff --git a/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/Data/a contact/that has been extended with @extend.json b/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/Data/a contact/that has been extended with @extend.json new file mode 100644 index 0000000..c386a1b --- /dev/null +++ b/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/Data/a contact/that has been extended with @extend.json @@ -0,0 +1,4 @@ +{ + "@extends": "../a contact", + "firstname": "Jane" +} \ No newline at end of file diff --git a/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/DataSteps.feature b/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/DataSteps.feature index 8ee5a61..1a51c9f 100644 --- a/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/DataSteps.feature +++ b/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/DataSteps.feature @@ -17,4 +17,7 @@ Scenario: Generate data at run-time with faker And I have opened 'the faked record' Scenario: Generate data as a named user - And 'an aliased user' has created 'a record with an alias' \ No newline at end of file + And 'an aliased user' has created 'a record with an alias' + +Scenario: Create data that extends from other data + And I have created 'a contact that has been extended with @extend' \ No newline at end of file diff --git a/templates/include-build-and-test-steps.yml b/templates/include-build-and-test-steps.yml index 6d8cadf..e587ee4 100644 --- a/templates/include-build-and-test-steps.yml +++ b/templates/include-build-and-test-steps.yml @@ -47,7 +47,7 @@ jobs: projectVersion: '$(GitVersion.SemVer)' extraProperties: | sonar.javascript.lcov.reportPaths=driver/test_results/coverage/lcov/lcov.info - sonar.coverage.exclusions=**\*spec.ts, bindings/tests/**/* + sonar.coverage.exclusions=**\*spec.ts, bindings/tests/**/*, bindings/src/Capgemini.PowerApps.SpecFlowBindings.MSBuild/**/* sonar.eslint.reportPaths=$(Build.SourcesDirectory)/driver/test_results/analysis/eslint.json - task: VSBuild@1 displayName: Build solution