Skip to content

Commit c9597cc

Browse files
Add metadata preprocessor to typewriter (#159)
* Added preprocessor and tests * Added CI badge to readme.
1 parent 29d15ca commit c9597cc

File tree

13 files changed

+676
-20
lines changed

13 files changed

+676
-20
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
[vipr-source-repo]: https://github.com/microsoft/vipr
22

3+
[![Build status](https://o365exchange.visualstudio.com/O365%20Sandbox/_apis/build/status/Microsoft%20Graph/msgraph-package-typewriter)](https://o365exchange.visualstudio.com/O365%20Sandbox/_build/latest?definitionId=1728)
4+
35
# Microsoft Graph SDK Code Generator
46

57
Source code writers for [VIPR][vipr-source-repo] utilizing T4 templates. The GraphODataTemplateWriter receives an OdcmModel from VIPR and uses it to fill in a T4 template located within this repository.

Typewriter.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vipr.Core", "submodules\vip
2626
EndProject
2727
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vipr.Reader.OData.v4", "submodules\vipr\src\Readers\Vipr.Reader.OData.v4\Vipr.Reader.OData.v4.csproj", "{6D8D8008-0A34-490F-8B38-21D9C8BF25C0}"
2828
EndProject
29+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Typewriter.Test", "test\Typewriter.Test\Typewriter.Test.csproj", "{4F1B8EF2-EAFD-4DDB-B688-1A134524047B}"
30+
EndProject
2931
Global
3032
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3133
Debug|Any CPU = Debug|Any CPU
@@ -54,6 +56,10 @@ Global
5456
{6D8D8008-0A34-490F-8B38-21D9C8BF25C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
5557
{6D8D8008-0A34-490F-8B38-21D9C8BF25C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
5658
{6D8D8008-0A34-490F-8B38-21D9C8BF25C0}.Release|Any CPU.Build.0 = Release|Any CPU
59+
{4F1B8EF2-EAFD-4DDB-B688-1A134524047B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
60+
{4F1B8EF2-EAFD-4DDB-B688-1A134524047B}.Debug|Any CPU.Build.0 = Debug|Any CPU
61+
{4F1B8EF2-EAFD-4DDB-B688-1A134524047B}.Release|Any CPU.ActiveCfg = Release|Any CPU
62+
{4F1B8EF2-EAFD-4DDB-B688-1A134524047B}.Release|Any CPU.Build.0 = Release|Any CPU
5763
EndGlobalSection
5864
GlobalSection(SolutionProperties) = preSolution
5965
HideSolutionNode = FALSE
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
using System;
2+
using System.Linq;
3+
using System.Runtime.CompilerServices;
4+
using System.Xml.Linq;
5+
using NLog;
6+
7+
namespace Typewriter
8+
{
9+
/// <summary>
10+
/// Contains a set of rules for altering CSDL metadata. These rules contain known hacks,
11+
/// fixes, and workarounds for issues in the metadata. Why the metadata has these issues
12+
/// is a long story.
13+
/// </summary>
14+
15+
internal class MetadataPreprocessor
16+
{
17+
private static Logger Logger => LogManager.GetLogger("MetadataPreprocessor");
18+
private static XDocument xMetadata;
19+
20+
21+
internal static XDocument GetXMetadata()
22+
{
23+
return xMetadata;
24+
}
25+
26+
// Added for tests.
27+
internal static void SetXMetadata(XDocument metadata)
28+
{
29+
xMetadata = metadata;
30+
}
31+
32+
/// <summary>
33+
/// Cleans metadata to match the assumptions in the generator and templates.
34+
/// </summary>
35+
/// <param name="csdlContents">Metadata content.</param>
36+
/// <returns>A string of metadata that should work with the generator.</returns>
37+
internal static string CleanMetadata(string csdlContents)
38+
{
39+
if (csdlContents == "")
40+
throw new ArgumentException("The CSDL string is empty.");
41+
42+
xMetadata = XDocument.Parse(csdlContents);
43+
44+
// Rules to apply to the {csdlContents} metadata.
45+
RemoveCapabilityAnnotations();
46+
AddLongDescriptionToThumbnail();
47+
RemoveHasStream("onenotePage");
48+
RemoveHasStream("onenoteResource");
49+
AddContainsTarget("plannerBucket");
50+
AddContainsTarget("plannerTask");
51+
AddContainsTarget("plannerPlan");
52+
AddContainsTarget("plannerDelta");
53+
54+
return xMetadata.ToString();
55+
}
56+
57+
/// <summary>
58+
/// Removes the HasStream attribute from an entity.
59+
/// We do this since the metadata does't properly describe the stream nature of this resources.
60+
/// Examples of this are the onenotePage and onenoteResource entities.
61+
/// </summary>
62+
/// <param name="entityTypeName">The value of an EntityType/Name attribute that will have the HasStream attribute removed.</param>
63+
internal static void RemoveHasStream(string entityTypeName)
64+
{
65+
try
66+
{
67+
xMetadata.Descendants()
68+
.Where(x => x.Name.LocalName == "EntityType")
69+
.Where(x => x.Attribute("Name").Value.Equals(entityTypeName))
70+
.ToList().ForEach(x => x.Attribute("HasStream").Remove());
71+
72+
Logger.Info("RemoveHasStream rule was applied so that we removed the HasStream attribute from the {0} entityType.", entityTypeName);
73+
}
74+
catch
75+
{
76+
Logger.Warn("RemoveHasStream rule was not applied so we could not remove the HasStream attribute from the {0} entityType.", entityTypeName);
77+
}
78+
}
79+
80+
/// <summary>
81+
/// Add ContainsTarget="true" to all navigationProperties whose type is a collection of a given entityType.
82+
/// This means that the navigation is to an entity and not to an entity reference.
83+
/// This also means that the contained entity is part of entity set of the parentEntity; an implied entity set.
84+
/// </summary>
85+
/// <param name="entityTypeName">The type of entity to self-contain</param>
86+
internal static void AddContainsTarget(string entityTypeName)
87+
{
88+
var list = xMetadata.Descendants()
89+
.Where(x => x.Name.LocalName == "NavigationProperty")
90+
.Where(x => x.Attribute("ContainsTarget") == null || x.Attribute("ContainsTarget").Value.Equals("false"))
91+
.Where(x => x.Attribute("Type").Value == "Collection(microsoft.graph." + entityTypeName + ")")
92+
.ToList();
93+
94+
if (list.Count == 0)
95+
{
96+
Logger.Warn("AddContainsTarget rule was not applied. No entity type named {0} found with missing navigation property containment.", entityTypeName);
97+
}
98+
else
99+
{
100+
list.ForEach(x =>
101+
{
102+
x.SetAttributeValue("ContainsTarget", "true");
103+
104+
var parentEntityName = x.Parent.Attribute("Name").Value;
105+
var navigationPropertyName = x.Attribute("Name").Value;
106+
107+
Logger.Info("AddContainsTarget rule applied so that ContainsTarget=true was set on the {0} entity's {1} navigation property.", parentEntityName, navigationPropertyName);
108+
});
109+
}
110+
}
111+
112+
/// <summary>
113+
/// Remove all capability annotations since the metadata doesn't describe
114+
/// them properly and the generator doesn't process them properly.
115+
/// </summary>
116+
internal static void RemoveCapabilityAnnotations()
117+
{
118+
xMetadata.Descendants()
119+
.Where(x => (string)x.Name.LocalName == "Annotation")
120+
.Where(x => x.Attribute("Term").Value.StartsWith("Org.OData.Capabilities"))
121+
.Remove();
122+
123+
Logger.Info("RemoveCapabilityAnnotations rule was applied so that capability annotations are removed from the metadata.");
124+
}
125+
126+
/// <summary>
127+
/// Adds a long description annotation to the thumbnail complex type. This annotation is a
128+
/// generation hint for the generator.
129+
/// </summary>
130+
internal static void AddLongDescriptionToThumbnail()
131+
{
132+
// Thumbnail hack - add LongDescription annotation
133+
XElement thumbnailComplexType = xMetadata.Descendants()
134+
.Where(x => (string)x.Name.LocalName == "ComplexType")
135+
.Where(x => x.Attribute("Name").Value == "thumbnail")
136+
.First();
137+
138+
if (thumbnailComplexType != null)
139+
{
140+
// need to specify namespace so default xmlns="" isn't added that breaks VIPR
141+
XElement thumbnailAnnotation = new XElement(thumbnailComplexType.Name.Namespace + "Annotation");
142+
143+
thumbnailAnnotation.Add(new XAttribute("Term", "Org.OData.Core.V1.LongDescription"));
144+
thumbnailAnnotation.Add(new XAttribute("String", "navigable"));
145+
thumbnailComplexType.Add(thumbnailAnnotation);
146+
147+
Logger.Info("AddLongDescriptionToThumbnail rule was applied to the thumbnail complex type.");
148+
}
149+
else
150+
{
151+
Logger.Error("AddLongDescriptionToThumbnail rule was not applied to the thumbnail complex type because the type wasn't found.");
152+
}
153+
}
154+
}
155+
}

src/Typewriter/Program.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,20 @@ static void Main(string[] args)
2525
.WithNotParsed((errs) => HandleError(errs));
2626
}
2727

28-
private static void GenerateSDK(Options opts)
28+
private static void GenerateSDK(Options options)
2929
{
3030
var stopwatch = new Stopwatch();
3131
stopwatch.Start();
3232

33-
SetupLogging(opts.Verbosity);
33+
SetupLogging(options.Verbosity);
3434

35-
var csdlContents = MetadataResolver.GetMetadata(opts.Metadata);
35+
var csdlContents = MetadataResolver.GetMetadata(options.Metadata);
3636

37-
// fix edmx
37+
// Clean up EDMX to work with the generators assumptions.
38+
var processCsdlContents = MetadataPreprocessor.CleanMetadata(csdlContents);
3839

39-
// filter out problem workloads
40-
41-
var files = MetadataToClientSource(csdlContents, opts.Language);
42-
FileWriter.WriteAsync(files,opts.Output);
40+
var files = MetadataToClientSource(processCsdlContents, options.Language);
41+
FileWriter.WriteAsync(files, options.Output);
4342

4443
stopwatch.Stop();
4544
Logger.Info($"Generation time: {stopwatch.Elapsed } seconds.");
Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Reflection;
1+
using System.Reflection;
22
using System.Runtime.CompilerServices;
33
using System.Runtime.InteropServices;
44

@@ -8,7 +8,7 @@
88
[assembly: AssemblyTitle("Typewriter")]
99
[assembly: AssemblyDescription("")]
1010
[assembly: AssemblyConfiguration("")]
11-
[assembly: AssemblyCompany("")]
11+
[assembly: AssemblyCompany("Microsoft")]
1212
[assembly: AssemblyProduct("Typewriter")]
1313
[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")]
1414
[assembly: AssemblyTrademark("")]
@@ -22,15 +22,6 @@
2222
// The following GUID is for the ID of the typelib if this project is exposed to COM
2323
[assembly: Guid("ca08f5d7-aa00-462e-be5e-e4f54c257fa5")]
2424

25-
// Version information for an assembly consists of the following four values:
26-
//
27-
// Major Version
28-
// Minor Version
29-
// Build Number
30-
// Revision
31-
//
32-
// You can specify all the values or you can default the Build and Revision Numbers
33-
// by using the '*' as shown below:
34-
// [assembly: AssemblyVersion("1.0.*")]
3525
[assembly: AssemblyVersion("0.1.0")]
3626
[assembly: AssemblyFileVersion("0.1.0")]
27+
[assembly: InternalsVisibleTo("Typewriter.Test")]

src/Typewriter/Typewriter.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
<ItemGroup>
4545
<Compile Include="ConfigurationProvider.cs" />
4646
<Compile Include="FileWriter.cs" />
47+
<Compile Include="MetadataPreprocessor.cs" />
4748
<Compile Include="MetadataResolver.cs" />
4849
<Compile Include="Options.cs" />
4950
<Compile Include="Program.cs" />
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using System;
2+
using System.Xml.Linq;
3+
using Microsoft.VisualStudio.TestTools.UnitTesting;
4+
using System.Text;
5+
using Typewriter;
6+
using System.Linq;
7+
using System.Collections.Generic;
8+
9+
namespace Typewriter.Test
10+
{
11+
[TestClass]
12+
public class MetadataPreprocessorTests
13+
{
14+
public string testMetadata;
15+
public XDocument testXMetadata;
16+
17+
/// <summary>
18+
/// Load metadata from file into a string so we can validate MetadataPreprocessor.
19+
/// </summary>
20+
[TestInitialize]
21+
public void Initialize()
22+
{
23+
testMetadata = Typewriter.Test.Properties.Resources.dirtyMetadata;
24+
testXMetadata = XDocument.Parse(testMetadata);
25+
MetadataPreprocessor.SetXMetadata(testXMetadata);
26+
}
27+
28+
[TestMethod]
29+
public void RemoveHasStreamTest()
30+
{
31+
var entityToProcess = "onenotePage";
32+
33+
bool hasStreamBefore = MetadataPreprocessor.GetXMetadata().Descendants()
34+
.Where(x => x.Name.LocalName == "EntityType")
35+
.Where(x => x.Attribute("Name").Value.Equals(entityToProcess))
36+
.Where(x => x.Attribute("HasStream").Value.Equals("true")).Any();
37+
38+
Assert.IsTrue(hasStreamBefore, "Expected: HasStream is present. Actual: HasStream was not found.");
39+
40+
MetadataPreprocessor.RemoveHasStream(entityToProcess);
41+
42+
bool hasStreamAfter = MetadataPreprocessor.GetXMetadata().Descendants()
43+
.Where(x => x.Name.LocalName == "EntityType")
44+
.Where(x => x.Attribute("Name").Value.Equals(entityToProcess))
45+
.Where(x => x.Attribute("HasStream") != null).Any();
46+
47+
Assert.IsFalse(hasStreamAfter, "Expected: The HasStream aatribute is not present. Actual: HasStream is present.");
48+
}
49+
50+
[TestMethod]
51+
public void AddContainsTargetTest()
52+
{
53+
var navPropTypeToProcess = "plannerPlan";
54+
55+
bool doesntContainTargetBefore = MetadataPreprocessor.GetXMetadata().Descendants()
56+
.Where(x => x.Name.LocalName == "NavigationProperty")
57+
.Where(x => x.Attribute("ContainsTarget") == null || x.Attribute("ContainsTarget").Value.Equals("false"))
58+
.Where(x => x.Attribute("Type").Value == "Collection(microsoft.graph." + navPropTypeToProcess + ")")
59+
.Any();
60+
61+
Assert.IsTrue(doesntContainTargetBefore, "Expected: ContainsTarget is false. Actual: ContainsTarget is true");
62+
63+
MetadataPreprocessor.AddContainsTarget(navPropTypeToProcess);
64+
65+
bool doesContainTargetAfter = MetadataPreprocessor.GetXMetadata().Descendants()
66+
.Where(x => x.Name.LocalName == "NavigationProperty")
67+
.Where(x => x.Attribute("ContainsTarget") != null)
68+
.Where(x => x.Attribute("ContainsTarget").Value == "true")
69+
.Where(x => x.Attribute("Type").Value == "Collection(microsoft.graph." + navPropTypeToProcess + ")")
70+
.Any();
71+
72+
Assert.IsTrue(doesContainTargetAfter, "Expected: ContainsTarget is true. Actual: ContainsTarget is false");
73+
}
74+
75+
[TestMethod]
76+
public void RemoveCapabilityAnnotationsTest()
77+
{
78+
bool hasCapabilityAnnotationsBefore = MetadataPreprocessor.GetXMetadata().Descendants()
79+
.Where(x => (string)x.Name.LocalName == "Annotation")
80+
.Where(x => x.Attribute("Term").Value.StartsWith("Org.OData.Capabilities")).Any();
81+
82+
MetadataPreprocessor.RemoveCapabilityAnnotations();
83+
84+
bool hasCapabilityAnnotationsAfter = MetadataPreprocessor.GetXMetadata().Descendants()
85+
.Where(x => (string)x.Name.LocalName == "Annotation")
86+
.Where(x => x.Attribute("Term").Value.StartsWith("Org.OData.Capabilities")).Any();
87+
88+
Assert.IsTrue(hasCapabilityAnnotationsBefore, "Expected: find capability annotations. Actual: found none."); // because the test data has capa annotations.
89+
Assert.IsFalse(hasCapabilityAnnotationsAfter, "Expected: false, there should be no elements returned. Actual: there are capability annotations."); //
90+
}
91+
92+
[TestMethod]
93+
public void AddLongDescriptionToThumbnailTest()
94+
{
95+
XElement thumbnailComplexTypeBefore = MetadataPreprocessor.GetXMetadata().Descendants()
96+
.Where(x => (string)x.Name.LocalName == "ComplexType")
97+
.Where(x => x.Attribute("Name").Value == "thumbnail")
98+
.First();
99+
100+
bool foundAnnotationBefore = thumbnailComplexTypeBefore.Descendants("{http://docs.oasis-open.org/odata/ns/edm}Annotation").Any();
101+
102+
Assert.IsNotNull(thumbnailComplexTypeBefore, "Expected: thumbnailComplexType is not null as the metadata contains this element. Actual: this element was not found in the metadata.");
103+
Assert.IsFalse(foundAnnotationBefore, "Expected: no annotation set before the addlong description rule. Actual: it has already been added");
104+
105+
MetadataPreprocessor.AddLongDescriptionToThumbnail();
106+
107+
XElement thumbnailComplexTypeAfter = MetadataPreprocessor.GetXMetadata().Descendants()
108+
.Where(x => (string)x.Name.LocalName == "ComplexType")
109+
.Where(x => x.Attribute("Name").Value == "thumbnail")
110+
.First();
111+
112+
bool foundAnnotationAfter = thumbnailComplexTypeAfter.Descendants("{http://docs.oasis-open.org/odata/ns/edm}Annotation")
113+
.Where(x => x.Attribute("String").Value.Equals("navigable")).Any();
114+
115+
Assert.IsTrue(foundAnnotationAfter, "Expected: thumbnailComplexType set with an annotation. Actual: annotation wasn't found.");
116+
}
117+
}
118+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.Reflection;
2+
using System.Runtime.CompilerServices;
3+
using System.Runtime.InteropServices;
4+
5+
[assembly: AssemblyTitle("Typewriter.Test")]
6+
[assembly: AssemblyDescription("")]
7+
[assembly: AssemblyConfiguration("")]
8+
[assembly: AssemblyCompany("")]
9+
[assembly: AssemblyProduct("Typewriter.Test")]
10+
[assembly: AssemblyCopyright("Copyright © 2018")]
11+
[assembly: AssemblyTrademark("")]
12+
[assembly: AssemblyCulture("")]
13+
14+
[assembly: ComVisible(false)]
15+
16+
[assembly: Guid("4f1b8ef2-eafd-4ddb-b688-1a134524047b")]
17+
18+
// [assembly: AssemblyVersion("1.0.*")]
19+
[assembly: AssemblyVersion("1.0.0.0")]
20+
[assembly: AssemblyFileVersion("1.0.0.0")]

0 commit comments

Comments
 (0)