Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/Typewriter/MetadataDefinitionType.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
117 changes: 113 additions & 4 deletions src/Typewriter/MetadataPreprocessor.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -11,7 +13,7 @@ namespace Typewriter
/// fixes, and workarounds for issues in the metadata. Why the metadata has these issues
/// is a long story.
/// </summary>

internal class MetadataPreprocessor
{
private static Logger Logger => LogManager.GetLogger("MetadataPreprocessor");
Expand Down Expand Up @@ -74,6 +76,19 @@ internal static string CleanMetadata(string csdlContents)
AddContainsTarget("appVulnerabilityManagedDevice");
AddContainsTarget("appVulnerabilityMobileApp");

ReorderElements(MetadataDefinitionType.Action,
"accept",
new List<string>() { "bindingParameter", "Comment", "SendResponse" },
"microsoft.graph.event");
ReorderElements(MetadataDefinitionType.Action,
"decline",
new List<string>() { "bindingParameter", "Comment", "SendResponse" },
"microsoft.graph.event");
ReorderElements(MetadataDefinitionType.Action,
"tentativelyAccept",
new List<string>() { "bindingParameter", "Comment", "SendResponse" },
"microsoft.graph.event");

return xMetadata.ToString();
}

Expand Down Expand Up @@ -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.");
}
}

/// <summary>
/// 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.
/// </summary>
/// <param name="metadataDefinitionType"></param>
/// <param name="targetGlobalElementName">The name of the element to target for reordering its child elements.</param>
/// <param name="newElementOrder">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.</param>
/// <param name="bindingParameterType">Specifies the type of the entity that is bound by the function identified
/// by targetGlobalElementName. Only applies to Actions and Functions.</param>
internal static void ReorderElements(MetadataDefinitionType metadataDefinitionType,
string targetGlobalElementName,
List<string> 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<XElement> newPropertyList = new List<XElement>();
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}");
}
}
}
}
4 changes: 3 additions & 1 deletion src/Typewriter/Options.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
3 changes: 2 additions & 1 deletion src/Typewriter/Typewriter.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<Compile Include="DocAnnotationWriter.cs" />
<Compile Include="FileWriter.cs" />
<Compile Include="Generator.cs" />
<Compile Include="MetadataDefinitionType.cs" />
<Compile Include="MetadataPreprocessor.cs" />
<Compile Include="MetadataResolver.cs" />
<Compile Include="Options.cs" />
Expand All @@ -71,7 +72,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ApiDoctor.Publishing">
<Version>1.0.0-CI-20181115-192733</Version>
<Version>1.0.0-CI-20191014-182007</Version>
</PackageReference>
<PackageReference Include="CommandLineParser">
<Version>2.2.1</Version>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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.");
}

/// <summary>
/// Tests that we reorder parameters according to an input listof parameters.
/// </summary>
[TestMethod]
public void It_reorders_parameters_in_an_action()
{
/* The element to reorder from the resources/dirtymetadata.xml file.
<Action Name="forward" IsBound="true">
<Parameter Name="bindingParameter" Type="microsoft.graph.onenotePage" />
<Parameter Name="ToRecipients" Type="Collection(microsoft.graph.recipient)" Nullable="false" />
<Parameter Name="Comment" Type="Edm.String" Unicode="false" />
</Action>
*/

// Specify the metadata definition to reorder and the new element order specification.
var targetMetadataDefType = MetadataDefinitionType.Action;
var targetMetadataDefName = "forward";
var newParameterOrder = new List<string>() { "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.");
}

/// <summary>
/// Tests that we reorder parameters according to an input element name list.
/// </summary>
[TestMethod]
public void It_reorders_elements_in_a_complextype()
{
/* The element to reorder from the resources/dirtymetadata.xml file.
<ComplexType Name="thumbnail">
<Property Name="content" Type="Edm.Stream" />
<Property Name="height" Type="Edm.Int32" />
<Property Name="sourceItemId" Type="Edm.String" />
<Property Name="url" Type="Edm.String" />
<Property Name="width" Type="Edm.Int32" />
</ComplexType>
*/

// Specify the metadata definition to reorder and the new element order specification.
var targetMetadataDefType = MetadataDefinitionType.ComplexType;
var targetMetadataDefName = "thumbnail";
var newParameterOrder = new List<string>() { "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.
<ComplexType Name="thumbnail">
<Property Name="content" Type="Edm.Stream" />
<Property Name="height" Type="Edm.Int32" />
<Property Name="sourceItemId" Type="Edm.String" />
<Property Name="url" Type="Edm.String" />
<Property Name="width" Type="Edm.Int32" />
</ComplexType>
*/

// Specify the metadata definition to reorder and the new
// element order specification.
var targetMetadataDefType = MetadataDefinitionType.ComplexType;
var targetMetadataDefName = "thumbnail";
var newParameterOrder = new List<string>() { "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.");
}
}
}
Loading