Skip to content

Commit 225cacf

Browse files
Add element reorder capability (#223)
* Added reordering logic. * Added tests, added rules. Fixes #222. * Add copyright, cleanup. * Target the bindingParameter, fix some long lines * Changes to ApiDoctor.Validation required to support adding doc annotations.
1 parent be3c6af commit 225cacf

File tree

6 files changed

+330
-8
lines changed

6 files changed

+330
-8
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
2+
3+
namespace Typewriter
4+
{
5+
public enum MetadataDefinitionType
6+
{
7+
EnumType,
8+
ComplexType,
9+
EntityType,
10+
Action,
11+
Function,
12+
Property,
13+
NavigationProperty,
14+
Member,
15+
EntitySet,
16+
EntityContainer,
17+
Singleton,
18+
NavigationPropertyBinding,
19+
Annotations,
20+
Annotation,
21+
Record,
22+
PropertyValue
23+
}
24+
}

src/Typewriter/MetadataPreprocessor.cs

Lines changed: 113 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
using System;
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
2+
3+
using NLog;
4+
using System;
5+
using System.Collections.Generic;
26
using System.Linq;
3-
using System.Runtime.CompilerServices;
47
using System.Xml.Linq;
5-
using NLog;
68

79
namespace Typewriter
810
{
@@ -11,7 +13,7 @@ namespace Typewriter
1113
/// fixes, and workarounds for issues in the metadata. Why the metadata has these issues
1214
/// is a long story.
1315
/// </summary>
14-
16+
1517
internal class MetadataPreprocessor
1618
{
1719
private static Logger Logger => LogManager.GetLogger("MetadataPreprocessor");
@@ -74,6 +76,19 @@ internal static string CleanMetadata(string csdlContents)
7476
AddContainsTarget("appVulnerabilityManagedDevice");
7577
AddContainsTarget("appVulnerabilityMobileApp");
7678

79+
ReorderElements(MetadataDefinitionType.Action,
80+
"accept",
81+
new List<string>() { "bindingParameter", "Comment", "SendResponse" },
82+
"microsoft.graph.event");
83+
ReorderElements(MetadataDefinitionType.Action,
84+
"decline",
85+
new List<string>() { "bindingParameter", "Comment", "SendResponse" },
86+
"microsoft.graph.event");
87+
ReorderElements(MetadataDefinitionType.Action,
88+
"tentativelyAccept",
89+
new List<string>() { "bindingParameter", "Comment", "SendResponse" },
90+
"microsoft.graph.event");
91+
7792
return xMetadata.ToString();
7893
}
7994

@@ -181,5 +196,99 @@ internal static void AddLongDescriptionToThumbnail()
181196
Logger.Error("AddLongDescriptionToThumbnail rule was not applied to the thumbnail complex type because the type wasn't found.");
182197
}
183198
}
199+
200+
/// <summary>
201+
/// Reorders a Microsoft Graph metadata element's child elements.
202+
/// Note: if we have to query and alter the metadata often, we may want to add a System.Action parameter to perform the query.
203+
/// </summary>
204+
/// <param name="metadataDefinitionType"></param>
205+
/// <param name="targetGlobalElementName">The name of the element to target for reordering its child elements.</param>
206+
/// <param name="newElementOrder">An ordered list of strings that represents the new order for the
207+
/// target element's child elements. Each entry string represents the name of an element.
208+
/// Each element in the list must match to a child element in the target global metadata element
209+
/// identified by targetGlobalElementName. This is particularly important for Actions and Functions
210+
/// as they may have overloads.</param>
211+
/// <param name="bindingParameterType">Specifies the type of the entity that is bound by the function identified
212+
/// by targetGlobalElementName. Only applies to Actions and Functions.</param>
213+
internal static void ReorderElements(MetadataDefinitionType metadataDefinitionType,
214+
string targetGlobalElementName,
215+
List<string> newElementOrder,
216+
string bindingParameterType = "")
217+
{
218+
// Actions or Functions require a binding element.
219+
if (String.IsNullOrEmpty(bindingParameterType) &&
220+
metadataDefinitionType == MetadataDefinitionType.Action ||
221+
metadataDefinitionType == MetadataDefinitionType.Function)
222+
{
223+
throw new ArgumentNullException(nameof(bindingParameterType),
224+
"The binding parameter type must be set in the case of an Action" +
225+
" or Function with the same name and parameter list.");
226+
}
227+
228+
// Validate that the specified new element order is meaningful.
229+
if (newElementOrder.Count < 2)
230+
{
231+
throw new ArgumentOutOfRangeException(nameof(newElementOrder),
232+
"ReorderElements: expected 2 or more elements to reorder.");
233+
}
234+
235+
// Sort the newElementOrder so we can compare the sequence to the potential overloads
236+
// returned for the the targetGlobalElementName. We should only ever find a single match.
237+
var newElementsAlphaOrdered = newElementOrder.OrderByDescending(x => x.ToString());
238+
239+
try
240+
{
241+
// Get the target element that has the target type (i.e. Action, EntityType), with the target Name (i.e. forward)
242+
// where it has the same element list as the new element list
243+
// where its binding parameter (the first element) has a Type attribute that matches the given
244+
// bindingParameterType in the case of Action or Function.
245+
246+
var results = xMetadata.Descendants()
247+
.Where(x => x.Name.LocalName == metadataDefinitionType.ToString())
248+
.Where(x => x.Attribute("Name").Value == targetGlobalElementName)
249+
.Where(el => el.Elements().Select(a => a.Attribute("Name").Value)
250+
.OrderByDescending(e => e.ToString())
251+
.SequenceEqual(newElementsAlphaOrdered));
252+
253+
XElement targetElement = null;
254+
255+
// Reordering elements by element Name attributes. Useful for non Action or Function.
256+
if (String.IsNullOrEmpty(bindingParameterType))
257+
{
258+
targetElement = results.FirstOrDefault();
259+
}
260+
else // We are reordering an Action or Function and must match the bindingParameterType.
261+
{
262+
targetElement = results.Where(e => e.Elements()
263+
.Take(1)
264+
.Any(a => a.Attribute("Type").Value == bindingParameterType))
265+
.FirstOrDefault();
266+
}
267+
268+
// There wasn't a match. We need to check our inputs.
269+
if (targetElement is null)
270+
throw new ArgumentException($"ReorderElements: Didn't find a {metadataDefinitionType.ToString()} " +
271+
$"named {targetGlobalElementName} that matched the elements in {nameof(newElementOrder)}");
272+
273+
// Reorder the elements
274+
List<XElement> newPropertyList = new List<XElement>();
275+
var propertyList = targetElement.Elements().ToList();
276+
foreach (string propertyName in newElementOrder)
277+
{
278+
var index = propertyList.FindIndex(x => x.Attribute("Name").Value == propertyName);
279+
newPropertyList.Add(propertyList[index]);
280+
}
281+
282+
// Update the metadata
283+
targetElement.Elements().Remove();
284+
targetElement.Add(newPropertyList);
285+
286+
Logger.Info($"Reordered the {targetGlobalElementName} {metadataDefinitionType.ToString()} child elements.");
287+
}
288+
catch (Exception ex)
289+
{
290+
Logger.Error($"ReorderElements exception caught.\r\nException message: {ex.Message}");
291+
}
292+
}
184293
}
185294
}

src/Typewriter/Options.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using CommandLine;
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
2+
3+
using CommandLine;
24
using System.Collections.Generic;
35
using System.Runtime.CompilerServices;
46

src/Typewriter/Typewriter.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
<Compile Include="DocAnnotationWriter.cs" />
4747
<Compile Include="FileWriter.cs" />
4848
<Compile Include="Generator.cs" />
49+
<Compile Include="MetadataDefinitionType.cs" />
4950
<Compile Include="MetadataPreprocessor.cs" />
5051
<Compile Include="MetadataResolver.cs" />
5152
<Compile Include="Options.cs" />
@@ -71,7 +72,7 @@
7172
</ItemGroup>
7273
<ItemGroup>
7374
<PackageReference Include="ApiDoctor.Publishing">
74-
<Version>1.0.0-CI-20181115-192733</Version>
75+
<Version>1.0.0-CI-20191014-182007</Version>
7576
</PackageReference>
7677
<PackageReference Include="CommandLineParser">
7778
<Version>2.2.1</Version>

test/Typewriter.Test/Given_a_valid_metadata_file_to_metadata_preprocessor.cs

Lines changed: 160 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
using Microsoft.VisualStudio.TestTools.UnitTesting;
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
2+
3+
using Microsoft.VisualStudio.TestTools.UnitTesting;
4+
using System.Collections.Generic;
25
using System.Linq;
36
using System.Xml.Linq;
47

@@ -50,7 +53,8 @@ public void It_adds_the_ContainsTarget_attribute()
5053

5154
bool doesntContainTargetBefore = MetadataPreprocessor.GetXMetadata().Descendants()
5255
.Where(x => x.Name.LocalName == "NavigationProperty")
53-
.Where(x => x.Attribute("ContainsTarget") == null || x.Attribute("ContainsTarget").Value.Equals("false"))
56+
.Where(x => x.Attribute("ContainsTarget") == null ||
57+
x.Attribute("ContainsTarget").Value.Equals("false"))
5458
.Where(x => x.Attribute("Type").Value == $"Collection(microsoft.graph.{navPropTypeToProcess})")
5559
.Any();
5660

@@ -110,5 +114,159 @@ public void It_adds_long_description_to_thumbnail()
110114

111115
Assert.IsTrue(foundAnnotationAfter, "Expected: thumbnailComplexType set with an annotation. Actual: annotation wasn't found.");
112116
}
117+
118+
/// <summary>
119+
/// Tests that we reorder parameters according to an input listof parameters.
120+
/// </summary>
121+
[TestMethod]
122+
public void It_reorders_parameters_in_an_action()
123+
{
124+
/* The element to reorder from the resources/dirtymetadata.xml file.
125+
<Action Name="forward" IsBound="true">
126+
<Parameter Name="bindingParameter" Type="microsoft.graph.onenotePage" />
127+
<Parameter Name="ToRecipients" Type="Collection(microsoft.graph.recipient)" Nullable="false" />
128+
<Parameter Name="Comment" Type="Edm.String" Unicode="false" />
129+
</Action>
130+
*/
131+
132+
// Specify the metadata definition to reorder and the new element order specification.
133+
var targetMetadataDefType = MetadataDefinitionType.Action;
134+
var targetMetadataDefName = "forward";
135+
var newParameterOrder = new List<string>() { "bindingParameter",
136+
"Comment",
137+
"ToRecipients" };
138+
var bindingParameterType = "microsoft.graph.onenotePage";
139+
140+
// Check whether an element exists in the metadata that matches our reordered element list before we reorder.
141+
var isTargetDefinitionInMetadataBefore = MetadataPreprocessor.GetXMetadata().Descendants()
142+
.Where(x => x.Name.LocalName == targetMetadataDefType.ToString())
143+
.Where(x => x.Attribute("Name").Value == targetMetadataDefName) // Returns all Action elements named forward.
144+
.Where(el => el.Descendants().FirstOrDefault(x => x.Attribute("Type").Value == bindingParameterType) != null)
145+
.Where(el => el.Elements().Select(a => a.Attribute("Name").Value)
146+
.SequenceEqual(newParameterOrder)).Any();
147+
148+
// Make a call to reorder the parameters for the target action in the metadata loaded into memory.
149+
MetadataPreprocessor.ReorderElements(targetMetadataDefType,
150+
targetMetadataDefName,
151+
newParameterOrder,
152+
bindingParameterType);
153+
154+
// Query the updated metadata for the results that match the reordered element.
155+
var results = MetadataPreprocessor.GetXMetadata().Descendants()
156+
.Where(x => x.Name.LocalName == targetMetadataDefType.ToString())
157+
.Where(x => x.Attribute("Name").Value == targetMetadataDefName) // Returns all Action elements named forward.
158+
.Where(el => el.Descendants().FirstOrDefault(x => x.Attribute("Type").Value == bindingParameterType) != null)
159+
.Where(el => el.Elements().Select(a => a.Attribute("Name").Value)
160+
.SequenceEqual(newParameterOrder));
161+
162+
Assert.IsFalse(isTargetDefinitionInMetadataBefore);
163+
// Added multiple elements with the same binding parameter - we want to make sure there is only one in the results.
164+
Assert.IsTrue(results.Count() == 1, $"Expected: A single result item. Actual: found {results.Count()} items.");
165+
Assert.AreEqual(newParameterOrder.Count(),
166+
results.FirstOrDefault().Elements().Count(),
167+
"The reordered element list doesn't match the count of elements in the input new order list.");
168+
Assert.IsTrue(results.FirstOrDefault()
169+
.Elements()
170+
.Select(a => a.Attribute("Name").Value)
171+
.SequenceEqual(newParameterOrder),
172+
"The element list was not reordered as expected.");
173+
}
174+
175+
/// <summary>
176+
/// Tests that we reorder parameters according to an input element name list.
177+
/// </summary>
178+
[TestMethod]
179+
public void It_reorders_elements_in_a_complextype()
180+
{
181+
/* The element to reorder from the resources/dirtymetadata.xml file.
182+
<ComplexType Name="thumbnail">
183+
<Property Name="content" Type="Edm.Stream" />
184+
<Property Name="height" Type="Edm.Int32" />
185+
<Property Name="sourceItemId" Type="Edm.String" />
186+
<Property Name="url" Type="Edm.String" />
187+
<Property Name="width" Type="Edm.Int32" />
188+
</ComplexType>
189+
*/
190+
191+
// Specify the metadata definition to reorder and the new element order specification.
192+
var targetMetadataDefType = MetadataDefinitionType.ComplexType;
193+
var targetMetadataDefName = "thumbnail";
194+
var newParameterOrder = new List<string>() { "width",
195+
"url",
196+
"sourceItemId",
197+
"height",
198+
"content" };
199+
200+
// Check whether an element exists in the metadata that
201+
// matches our reordered element list before we reorder.
202+
var isTargetDefinitionInMetadataBefore = MetadataPreprocessor.GetXMetadata().Descendants()
203+
.Where(x => x.Name.LocalName == targetMetadataDefType.ToString())
204+
.Where(x => x.Attribute("Name").Value == targetMetadataDefName)
205+
.Where(el => el.Elements().Select(a => a.Attribute("Name").Value)
206+
.SequenceEqual(newParameterOrder)).Any();
207+
208+
// Make a call to reorder the parameters for the target
209+
// complex type in the metadata loaded into memory.
210+
MetadataPreprocessor.ReorderElements(targetMetadataDefType,
211+
targetMetadataDefName,
212+
newParameterOrder);
213+
214+
// Query the updated metadata for the results that match the reordered element.
215+
var results = MetadataPreprocessor.GetXMetadata().Descendants()
216+
.Where(x => x.Name.LocalName == targetMetadataDefType.ToString())
217+
.Where(x => x.Attribute("Name").Value == targetMetadataDefName)
218+
.Where(el => el.Elements().Select(a => a.Attribute("Name").Value)
219+
.SequenceEqual(newParameterOrder));
220+
221+
Assert.IsFalse(isTargetDefinitionInMetadataBefore);
222+
Assert.IsTrue(results.Count() == 1, $"Expected: A single result item. Actual: found {results.Count()} items.");
223+
Assert.AreEqual(newParameterOrder.Count(),
224+
results.FirstOrDefault().Elements().Count(),
225+
"The reordered element list doesn't match the count of elements in the input new order list.");
226+
Assert.IsTrue(results.FirstOrDefault().Elements().Select(a => a.Attribute("Name").Value).SequenceEqual(newParameterOrder),
227+
"The element list was not reordered as expected.");
228+
}
229+
230+
[TestMethod]
231+
public void It_does_not_reorder_when_element_list_does_not_match_in_a_complextype()
232+
{
233+
/* The element to attempt to reorder from the resources/dirtymetadata.xml file.
234+
* The element list, newParameterOrder does not match the thumbnail type
235+
* in the metadata (missing 'content' element) so we expect that the
236+
* reorder attempt fails.
237+
<ComplexType Name="thumbnail">
238+
<Property Name="content" Type="Edm.Stream" />
239+
<Property Name="height" Type="Edm.Int32" />
240+
<Property Name="sourceItemId" Type="Edm.String" />
241+
<Property Name="url" Type="Edm.String" />
242+
<Property Name="width" Type="Edm.Int32" />
243+
</ComplexType>
244+
*/
245+
246+
// Specify the metadata definition to reorder and the new
247+
// element order specification.
248+
var targetMetadataDefType = MetadataDefinitionType.ComplexType;
249+
var targetMetadataDefName = "thumbnail";
250+
var newParameterOrder = new List<string>() { "width",
251+
"url",
252+
"sourceItemId",
253+
"height" };
254+
255+
// Make a call to reorder the parameters for the target
256+
// complex type in the metadata loaded into memory.
257+
MetadataPreprocessor.ReorderElements(targetMetadataDefType,
258+
targetMetadataDefName,
259+
newParameterOrder);
260+
261+
// Query the updated metadata for the results that match the reordered element.
262+
var results = MetadataPreprocessor.GetXMetadata().Descendants()
263+
.Where(x => x.Name.LocalName == targetMetadataDefType.ToString())
264+
.Where(x => x.Attribute("Name").Value == targetMetadataDefName)
265+
.Where(el => el.Elements().Select(a => a.Attribute("Name").Value)
266+
.SequenceEqual(newParameterOrder));
267+
268+
Assert.IsTrue(results.Count() == 0,
269+
$"Expected: Zero results. Actual: found {results.Count()} items.");
270+
}
113271
}
114272
}

0 commit comments

Comments
 (0)