diff --git a/src/Typewriter/MetadataDefinitionType.cs b/src/Typewriter/MetadataDefinitionType.cs new file mode 100644 index 000000000..899d3b518 --- /dev/null +++ b/src/Typewriter/MetadataDefinitionType.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. + +namespace Typewriter +{ + public enum MetadataDefinitionType + { + EnumType, + ComplexType, + EntityType, + Action, + Function, + Property, + NavigationProperty, + Member, + EntitySet, + EntityContainer, + Singleton, + NavigationPropertyBinding, + Annotations, + Annotation, + Record, + PropertyValue + } +} diff --git a/src/Typewriter/MetadataPreprocessor.cs b/src/Typewriter/MetadataPreprocessor.cs index 004b62e67..7aaefa019 100644 --- a/src/Typewriter/MetadataPreprocessor.cs +++ b/src/Typewriter/MetadataPreprocessor.cs @@ -1,8 +1,10 @@ -using System; +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. + +using NLog; +using System; +using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; using System.Xml.Linq; -using NLog; namespace Typewriter { @@ -11,7 +13,7 @@ namespace Typewriter /// fixes, and workarounds for issues in the metadata. Why the metadata has these issues /// is a long story. /// - + internal class MetadataPreprocessor { private static Logger Logger => LogManager.GetLogger("MetadataPreprocessor"); @@ -74,6 +76,19 @@ internal static string CleanMetadata(string csdlContents) AddContainsTarget("appVulnerabilityManagedDevice"); AddContainsTarget("appVulnerabilityMobileApp"); + ReorderElements(MetadataDefinitionType.Action, + "accept", + new List() { "bindingParameter", "Comment", "SendResponse" }, + "microsoft.graph.event"); + ReorderElements(MetadataDefinitionType.Action, + "decline", + new List() { "bindingParameter", "Comment", "SendResponse" }, + "microsoft.graph.event"); + ReorderElements(MetadataDefinitionType.Action, + "tentativelyAccept", + new List() { "bindingParameter", "Comment", "SendResponse" }, + "microsoft.graph.event"); + return xMetadata.ToString(); } @@ -181,5 +196,99 @@ internal static void AddLongDescriptionToThumbnail() Logger.Error("AddLongDescriptionToThumbnail rule was not applied to the thumbnail complex type because the type wasn't found."); } } + + /// + /// Reorders a Microsoft Graph metadata element's child elements. + /// Note: if we have to query and alter the metadata often, we may want to add a System.Action parameter to perform the query. + /// + /// + /// The name of the element to target for reordering its child elements. + /// An ordered list of strings that represents the new order for the + /// target element's child elements. Each entry string represents the name of an element. + /// Each element in the list must match to a child element in the target global metadata element + /// identified by targetGlobalElementName. This is particularly important for Actions and Functions + /// as they may have overloads. + /// Specifies the type of the entity that is bound by the function identified + /// by targetGlobalElementName. Only applies to Actions and Functions. + internal static void ReorderElements(MetadataDefinitionType metadataDefinitionType, + string targetGlobalElementName, + List newElementOrder, + string bindingParameterType = "") + { + // Actions or Functions require a binding element. + if (String.IsNullOrEmpty(bindingParameterType) && + metadataDefinitionType == MetadataDefinitionType.Action || + metadataDefinitionType == MetadataDefinitionType.Function) + { + throw new ArgumentNullException(nameof(bindingParameterType), + "The binding parameter type must be set in the case of an Action" + + " or Function with the same name and parameter list."); + } + + // Validate that the specified new element order is meaningful. + if (newElementOrder.Count < 2) + { + throw new ArgumentOutOfRangeException(nameof(newElementOrder), + "ReorderElements: expected 2 or more elements to reorder."); + } + + // Sort the newElementOrder so we can compare the sequence to the potential overloads + // returned for the the targetGlobalElementName. We should only ever find a single match. + var newElementsAlphaOrdered = newElementOrder.OrderByDescending(x => x.ToString()); + + try + { + // Get the target element that has the target type (i.e. Action, EntityType), with the target Name (i.e. forward) + // where it has the same element list as the new element list + // where its binding parameter (the first element) has a Type attribute that matches the given + // bindingParameterType in the case of Action or Function. + + var results = xMetadata.Descendants() + .Where(x => x.Name.LocalName == metadataDefinitionType.ToString()) + .Where(x => x.Attribute("Name").Value == targetGlobalElementName) + .Where(el => el.Elements().Select(a => a.Attribute("Name").Value) + .OrderByDescending(e => e.ToString()) + .SequenceEqual(newElementsAlphaOrdered)); + + XElement targetElement = null; + + // Reordering elements by element Name attributes. Useful for non Action or Function. + if (String.IsNullOrEmpty(bindingParameterType)) + { + targetElement = results.FirstOrDefault(); + } + else // We are reordering an Action or Function and must match the bindingParameterType. + { + targetElement = results.Where(e => e.Elements() + .Take(1) + .Any(a => a.Attribute("Type").Value == bindingParameterType)) + .FirstOrDefault(); + } + + // There wasn't a match. We need to check our inputs. + if (targetElement is null) + throw new ArgumentException($"ReorderElements: Didn't find a {metadataDefinitionType.ToString()} " + + $"named {targetGlobalElementName} that matched the elements in {nameof(newElementOrder)}"); + + // Reorder the elements + List newPropertyList = new List(); + var propertyList = targetElement.Elements().ToList(); + foreach (string propertyName in newElementOrder) + { + var index = propertyList.FindIndex(x => x.Attribute("Name").Value == propertyName); + newPropertyList.Add(propertyList[index]); + } + + // Update the metadata + targetElement.Elements().Remove(); + targetElement.Add(newPropertyList); + + Logger.Info($"Reordered the {targetGlobalElementName} {metadataDefinitionType.ToString()} child elements."); + } + catch (Exception ex) + { + Logger.Error($"ReorderElements exception caught.\r\nException message: {ex.Message}"); + } + } } } \ No newline at end of file diff --git a/src/Typewriter/Options.cs b/src/Typewriter/Options.cs index a8ed3ec1b..fe55bbcc7 100644 --- a/src/Typewriter/Options.cs +++ b/src/Typewriter/Options.cs @@ -1,4 +1,6 @@ -using CommandLine; +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. + +using CommandLine; using System.Collections.Generic; using System.Runtime.CompilerServices; diff --git a/src/Typewriter/Typewriter.csproj b/src/Typewriter/Typewriter.csproj index aae5f9ecd..d9b2fc2b7 100644 --- a/src/Typewriter/Typewriter.csproj +++ b/src/Typewriter/Typewriter.csproj @@ -46,6 +46,7 @@ + @@ -71,7 +72,7 @@ - 1.0.0-CI-20181115-192733 + 1.0.0-CI-20191014-182007 2.2.1 diff --git a/test/Typewriter.Test/Given_a_valid_metadata_file_to_metadata_preprocessor.cs b/test/Typewriter.Test/Given_a_valid_metadata_file_to_metadata_preprocessor.cs index 35fbfb221..67c65f831 100644 --- a/test/Typewriter.Test/Given_a_valid_metadata_file_to_metadata_preprocessor.cs +++ b/test/Typewriter.Test/Given_a_valid_metadata_file_to_metadata_preprocessor.cs @@ -1,4 +1,7 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -50,7 +53,8 @@ public void It_adds_the_ContainsTarget_attribute() bool doesntContainTargetBefore = MetadataPreprocessor.GetXMetadata().Descendants() .Where(x => x.Name.LocalName == "NavigationProperty") - .Where(x => x.Attribute("ContainsTarget") == null || x.Attribute("ContainsTarget").Value.Equals("false")) + .Where(x => x.Attribute("ContainsTarget") == null || + x.Attribute("ContainsTarget").Value.Equals("false")) .Where(x => x.Attribute("Type").Value == $"Collection(microsoft.graph.{navPropTypeToProcess})") .Any(); @@ -110,5 +114,159 @@ public void It_adds_long_description_to_thumbnail() Assert.IsTrue(foundAnnotationAfter, "Expected: thumbnailComplexType set with an annotation. Actual: annotation wasn't found."); } + + /// + /// Tests that we reorder parameters according to an input listof parameters. + /// + [TestMethod] + public void It_reorders_parameters_in_an_action() + { + /* The element to reorder from the resources/dirtymetadata.xml file. + + + + + + */ + + // Specify the metadata definition to reorder and the new element order specification. + var targetMetadataDefType = MetadataDefinitionType.Action; + var targetMetadataDefName = "forward"; + var newParameterOrder = new List() { "bindingParameter", + "Comment", + "ToRecipients" }; + var bindingParameterType = "microsoft.graph.onenotePage"; + + // Check whether an element exists in the metadata that matches our reordered element list before we reorder. + var isTargetDefinitionInMetadataBefore = MetadataPreprocessor.GetXMetadata().Descendants() + .Where(x => x.Name.LocalName == targetMetadataDefType.ToString()) + .Where(x => x.Attribute("Name").Value == targetMetadataDefName) // Returns all Action elements named forward. + .Where(el => el.Descendants().FirstOrDefault(x => x.Attribute("Type").Value == bindingParameterType) != null) + .Where(el => el.Elements().Select(a => a.Attribute("Name").Value) + .SequenceEqual(newParameterOrder)).Any(); + + // Make a call to reorder the parameters for the target action in the metadata loaded into memory. + MetadataPreprocessor.ReorderElements(targetMetadataDefType, + targetMetadataDefName, + newParameterOrder, + bindingParameterType); + + // Query the updated metadata for the results that match the reordered element. + var results = MetadataPreprocessor.GetXMetadata().Descendants() + .Where(x => x.Name.LocalName == targetMetadataDefType.ToString()) + .Where(x => x.Attribute("Name").Value == targetMetadataDefName) // Returns all Action elements named forward. + .Where(el => el.Descendants().FirstOrDefault(x => x.Attribute("Type").Value == bindingParameterType) != null) + .Where(el => el.Elements().Select(a => a.Attribute("Name").Value) + .SequenceEqual(newParameterOrder)); + + Assert.IsFalse(isTargetDefinitionInMetadataBefore); + // Added multiple elements with the same binding parameter - we want to make sure there is only one in the results. + Assert.IsTrue(results.Count() == 1, $"Expected: A single result item. Actual: found {results.Count()} items."); + Assert.AreEqual(newParameterOrder.Count(), + results.FirstOrDefault().Elements().Count(), + "The reordered element list doesn't match the count of elements in the input new order list."); + Assert.IsTrue(results.FirstOrDefault() + .Elements() + .Select(a => a.Attribute("Name").Value) + .SequenceEqual(newParameterOrder), + "The element list was not reordered as expected."); + } + + /// + /// Tests that we reorder parameters according to an input element name list. + /// + [TestMethod] + public void It_reorders_elements_in_a_complextype() + { + /* The element to reorder from the resources/dirtymetadata.xml file. + + + + + + + + */ + + // Specify the metadata definition to reorder and the new element order specification. + var targetMetadataDefType = MetadataDefinitionType.ComplexType; + var targetMetadataDefName = "thumbnail"; + var newParameterOrder = new List() { "width", + "url", + "sourceItemId", + "height", + "content" }; + + // Check whether an element exists in the metadata that + // matches our reordered element list before we reorder. + var isTargetDefinitionInMetadataBefore = MetadataPreprocessor.GetXMetadata().Descendants() + .Where(x => x.Name.LocalName == targetMetadataDefType.ToString()) + .Where(x => x.Attribute("Name").Value == targetMetadataDefName) + .Where(el => el.Elements().Select(a => a.Attribute("Name").Value) + .SequenceEqual(newParameterOrder)).Any(); + + // Make a call to reorder the parameters for the target + // complex type in the metadata loaded into memory. + MetadataPreprocessor.ReorderElements(targetMetadataDefType, + targetMetadataDefName, + newParameterOrder); + + // Query the updated metadata for the results that match the reordered element. + var results = MetadataPreprocessor.GetXMetadata().Descendants() + .Where(x => x.Name.LocalName == targetMetadataDefType.ToString()) + .Where(x => x.Attribute("Name").Value == targetMetadataDefName) + .Where(el => el.Elements().Select(a => a.Attribute("Name").Value) + .SequenceEqual(newParameterOrder)); + + Assert.IsFalse(isTargetDefinitionInMetadataBefore); + Assert.IsTrue(results.Count() == 1, $"Expected: A single result item. Actual: found {results.Count()} items."); + Assert.AreEqual(newParameterOrder.Count(), + results.FirstOrDefault().Elements().Count(), + "The reordered element list doesn't match the count of elements in the input new order list."); + Assert.IsTrue(results.FirstOrDefault().Elements().Select(a => a.Attribute("Name").Value).SequenceEqual(newParameterOrder), + "The element list was not reordered as expected."); + } + + [TestMethod] + public void It_does_not_reorder_when_element_list_does_not_match_in_a_complextype() + { + /* The element to attempt to reorder from the resources/dirtymetadata.xml file. + * The element list, newParameterOrder does not match the thumbnail type + * in the metadata (missing 'content' element) so we expect that the + * reorder attempt fails. + + + + + + + + */ + + // Specify the metadata definition to reorder and the new + // element order specification. + var targetMetadataDefType = MetadataDefinitionType.ComplexType; + var targetMetadataDefName = "thumbnail"; + var newParameterOrder = new List() { "width", + "url", + "sourceItemId", + "height" }; + + // Make a call to reorder the parameters for the target + // complex type in the metadata loaded into memory. + MetadataPreprocessor.ReorderElements(targetMetadataDefType, + targetMetadataDefName, + newParameterOrder); + + // Query the updated metadata for the results that match the reordered element. + var results = MetadataPreprocessor.GetXMetadata().Descendants() + .Where(x => x.Name.LocalName == targetMetadataDefType.ToString()) + .Where(x => x.Attribute("Name").Value == targetMetadataDefName) + .Where(el => el.Elements().Select(a => a.Attribute("Name").Value) + .SequenceEqual(newParameterOrder)); + + Assert.IsTrue(results.Count() == 0, + $"Expected: Zero results. Actual: found {results.Count()} items."); + } } } diff --git a/test/Typewriter.Test/Resources/dirtyMetadata.xml b/test/Typewriter.Test/Resources/dirtyMetadata.xml index 6ccafb280..052e90aca 100644 --- a/test/Typewriter.Test/Resources/dirtyMetadata.xml +++ b/test/Typewriter.Test/Resources/dirtyMetadata.xml @@ -29,6 +29,10 @@ + + + + @@ -37,6 +41,30 @@ + + + + + + + + + + + + + + + + + + + + + + + +