Skip to content

Commit 2669160

Browse files
committed
feat: Create YAML Writer
1 parent 2971244 commit 2669160

15 files changed

+728
-98
lines changed

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Amazon.Lambda.Annotations.SourceGenerator.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<ItemGroup>
1818
<!-- Generator dependencies, update Libraries/src/Amazon.Lambda.Annotations.nuspec whenever a new generator dependency is added. -->
1919
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" GeneratePathProperty="true" PrivateAssets="all" />
20+
<PackageReference Include="YamlDotNet" Version="12.0.0" GeneratePathProperty="true" PrivateAssets="all" />
2021
</ItemGroup>
2122

2223
<PropertyGroup>
@@ -26,6 +27,7 @@
2627
<Target Name="GetDependencyTargetPaths">
2728
<ItemGroup>
2829
<TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" />
30+
<TargetPathWithTargetPlatformMoniker Include="$(PKGYamlDotNet)\lib\netstandard2.0\YamlDotNet.dll" IncludeRuntimeDependency="false" />
2931
</ItemGroup>
3032
</Target>
3133

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public class Generator : ISourceGenerator
2222
{
2323
private readonly IFileManager _fileManager = new FileManager();
2424
private readonly IDirectoryManager _directoryManager = new DirectoryManager();
25-
private readonly IJsonWriter _jsonWriter = new JsonWriter();
25+
private readonly ITemplateWriter _jsonWriter = new JsonWriter();
2626

2727
public Generator()
2828
{
@@ -74,7 +74,7 @@ public void Execute(GeneratorExecutionContext context)
7474
foreach (var lambdaMethod in receiver.LambdaMethods)
7575
{
7676
var lambdaMethodModel = semanticModelProvider.GetMethodSemanticModel(lambdaMethod);
77-
77+
7878
// Check for necessary references
7979
if (lambdaMethodModel.HasAttribute(context, TypeFullNames.RestApiAttribute)
8080
|| lambdaMethodModel.HasAttribute(context, TypeFullNames.HttpApiAttribute))
@@ -87,7 +87,7 @@ public void Execute(GeneratorExecutionContext context)
8787
"Amazon.Lambda.APIGatewayEvents"));
8888
}
8989
}
90-
90+
9191
var model = LambdaFunctionModelBuilder.Build(lambdaMethodModel, configureMethodModel, context);
9292

9393
// If there are more than one event, report them as errors

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationJsonWriter.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ public class CloudFormationJsonWriter : IAnnotationReportWriter
1616
{
1717
private readonly IFileManager _fileManager;
1818
private readonly IDirectoryManager _directoryManager;
19-
private readonly IJsonWriter _jsonWriter;
19+
private readonly ITemplateWriter _jsonWriter;
2020
private readonly IDiagnosticReporter _diagnosticReporter;
2121

22-
public CloudFormationJsonWriter(IFileManager fileManager, IDirectoryManager directoryManager, IJsonWriter jsonWriter, IDiagnosticReporter diagnosticReporter)
22+
public CloudFormationJsonWriter(IFileManager fileManager, IDirectoryManager directoryManager, ITemplateWriter jsonWriter, IDiagnosticReporter diagnosticReporter)
2323
{
2424
_fileManager = fileManager;
2525
_directoryManager = directoryManager;
@@ -49,7 +49,7 @@ public void ApplyReport(AnnotationReport report)
4949
}
5050

5151
RemoveOrphanedLambdaFunctions(processedLambdaFunctions);
52-
var json = _jsonWriter.GetPrettyJson();
52+
var json = _jsonWriter.GetContent();
5353
_fileManager.WriteAllText(report.CloudFormationTemplatePath, json);
5454

5555
_diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.CodeGeneration, Location.None, $"{report.CloudFormationTemplatePath}", json));
@@ -62,8 +62,8 @@ private bool ShouldProcessLambdaFunction(ILambdaFunctionSerializable lambdaFunct
6262
if (!_jsonWriter.Exists(lambdaFunctionPath))
6363
return true;
6464

65-
var creationTool = _jsonWriter.GetToken($"{lambdaFunctionPath}.Metadata.Tool", string.Empty);
66-
return string.Equals(creationTool.ToObject<string>(), "Amazon.Lambda.Annotations", StringComparison.Ordinal);
65+
var creationTool = _jsonWriter.GetToken<string>($"{lambdaFunctionPath}.Metadata.Tool", string.Empty);
66+
return string.Equals(creationTool, "Amazon.Lambda.Annotations", StringComparison.Ordinal);
6767
}
6868

6969
private void ProcessLambdaFunction(ILambdaFunctionSerializable lambdaFunction, string relativeProjectUri)
@@ -95,7 +95,7 @@ private void ProcessLambdaFunctionAttributes(ILambdaFunctionSerializable lambdaF
9595
if (!string.IsNullOrEmpty(lambdaFunction.Policies))
9696
{
9797
var policyArray = lambdaFunction.Policies.Split(',').Select(x => GetValueOrRef(x.Trim()));
98-
_jsonWriter.SetToken($"{propertiesPath}.Policies", new JArray(policyArray));
98+
_jsonWriter.SetToken($"{propertiesPath}.Policies", new JArray(policyArray), TokenType.List);
9999
_jsonWriter.RemoveToken($"{propertiesPath}.Role");
100100
}
101101

@@ -117,7 +117,7 @@ private void ProcessPackageTypeProperty(ILambdaFunctionSerializable lambdaFuncti
117117

118118
case LambdaPackageType.Image:
119119
_jsonWriter.SetToken($"{propertiesPath}.ImageUri", relativeProjectUri);
120-
_jsonWriter.SetToken($"{propertiesPath}.ImageConfig.Command", new JArray(lambdaFunction.Handler));
120+
_jsonWriter.SetToken($"{propertiesPath}.ImageConfig.Command", new JArray(lambdaFunction.Handler), TokenType.List);
121121
_jsonWriter.RemoveToken($"{propertiesPath}.Handler");
122122
_jsonWriter.RemoveToken($"{propertiesPath}.CodeUri");
123123
_jsonWriter.RemoveToken($"{propertiesPath}.Runtime");
@@ -161,7 +161,7 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la
161161
}
162162

163163
if (currentSyncedEvents.Any())
164-
_jsonWriter.SetToken(syncedEventsMetadataPath, new JArray(currentSyncedEvents));
164+
_jsonWriter.SetToken(syncedEventsMetadataPath, new JArray(currentSyncedEvents), TokenType.List);
165165
else
166166
_jsonWriter.RemoveToken(syncedEventsMetadataPath);
167167
}
@@ -203,7 +203,7 @@ private void ApplyLambdaFunctionDefaults(string lambdaFunctionPath, string prope
203203
_jsonWriter.SetToken($"{propertiesPath}.CodeUri", "");
204204
_jsonWriter.SetToken($"{propertiesPath}.MemorySize", 256);
205205
_jsonWriter.SetToken($"{propertiesPath}.Timeout", 30);
206-
_jsonWriter.SetToken($"{propertiesPath}.Policies", new JArray("AWSLambdaBasicExecutionRole"));
206+
_jsonWriter.SetToken($"{propertiesPath}.Policies", new JArray("AWSLambdaBasicExecutionRole"), TokenType.List);
207207
}
208208

209209
private void CreateNewTemplate()
@@ -222,11 +222,11 @@ private void RemoveOrphanedLambdaFunctions(HashSet<string> processedLambdaFuncti
222222
foreach (var resource in resourceToken.Properties())
223223
{
224224
var resourcePath = $"Resources.{resource.Name}";
225-
var type = _jsonWriter.GetToken($"{resourcePath}.Type", string.Empty);
226-
var creationTool = _jsonWriter.GetToken($"{resourcePath}.Metadata.Tool", string.Empty);
225+
var type = _jsonWriter.GetToken<string>($"{resourcePath}.Type", string.Empty);
226+
var creationTool = _jsonWriter.GetToken<string>($"{resourcePath}.Metadata.Tool", string.Empty);
227227

228-
if (string.Equals(type.ToObject<string>(), "AWS::Serverless::Function", StringComparison.Ordinal)
229-
&& string.Equals(creationTool.ToObject<string>(), "Amazon.Lambda.Annotations", StringComparison.Ordinal)
228+
if (string.Equals(type, "AWS::Serverless::Function", StringComparison.Ordinal)
229+
&& string.Equals(creationTool, "Amazon.Lambda.Annotations", StringComparison.Ordinal)
230230
&& !processedLambdaFunctions.Contains(resource.Name))
231231
{
232232
toRemove.Add(resource.Name);

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/IJsonWriter.cs

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
namespace Amazon.Lambda.Annotations.SourceGenerator.Writers
2+
{
3+
/// <summary>
4+
/// This interface contains utility methods to manipulate a YAML or JSON blob
5+
/// </summary>
6+
public interface ITemplateWriter
7+
{
8+
/// <summary>
9+
/// Checks if the dot(.) seperated path exists in the blob
10+
/// </summary>
11+
/// <param name="path">dot(.) seperated path. Example "Person.Name.FirstName"</param>
12+
/// <returns>true if the path exist, else false</returns>
13+
bool Exists(string path);
14+
15+
/// <summary>
16+
/// Gets the object stored at the dot(.) seperated path. If the path does not exist then return the defaultToken.
17+
/// </summary>
18+
/// <param name="path">dot(.) seperated path. Example "Person.Name.FirstName"</param>
19+
/// <param name="defaultToken">The object that is returned if path does not exist.</param>
20+
object GetToken(string path, object defaultToken = null);
21+
22+
/// <summary>
23+
/// Gets the object stored at the dot(.) seperated path. If the path does not exist then return the defaultToken.
24+
/// </summary>
25+
/// <param name="path">dot(.) seperated path. Example "Person.Name.FirstName"</param>
26+
/// <param name="defaultToken">The object that is returned if path does not exist.</param>
27+
T GetToken<T>(string path, object defaultToken = null);
28+
29+
/// <summary>
30+
/// Sets the token at the dot(.) seperated path.
31+
/// </summary>
32+
/// <param name="path">dot(.) seperated path. Example "Person.Name.FirstName"</param>
33+
/// <param name="token">The object to set at the specified path</param>
34+
/// <param name="tokenType"><see cref="TokenType"/></param>
35+
void SetToken(string path, object token, TokenType tokenType = TokenType.Other);
36+
37+
/// <summary>
38+
/// Deletes the token found at the dot(.) separated path.
39+
/// </summary>
40+
/// <param name="path">dot(.) seperated path. Example "Person.Name.FirstName"</param>
41+
void RemoveToken(string path);
42+
43+
/// <summary>
44+
/// Returns the template as a string
45+
/// </summary>
46+
string GetContent();
47+
48+
/// <summary>
49+
/// Converts the content into an in-memory representation of JSON or YAML node
50+
/// </summary>
51+
/// <param name="content"></param>
52+
void Parse(string content);
53+
}
54+
}

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/JsonWriter.cs

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66

77
namespace Amazon.Lambda.Annotations.SourceGenerator.Writers
88
{
9-
public class JsonWriter : IJsonWriter
9+
/// <summary>
10+
/// This contains the functionality to manipulate a JSON blob
11+
/// </summary>
12+
public class JsonWriter : ITemplateWriter
1013
{
1114
private JObject _rootNode;
1215

@@ -15,18 +18,19 @@ public JsonWriter()
1518
_rootNode = new JObject();
1619
}
1720

18-
public JsonWriter(JObject rootNode)
19-
{
20-
_rootNode = rootNode;
21-
}
22-
21+
/// <summary>
22+
/// Checks if the dot(.) seperated jsonPath exists in the json blob stored at the _rootNode
23+
/// </summary>
24+
/// <param name="jsonPath">dot(.) seperated path. Example "Person.Name.FirstName"</param>
25+
/// <returns>true if the path exist, else false</returns>
26+
/// <exception cref="InvalidDataException">Thrown if the jsonPath is invalid</exception>
2327
public bool Exists(string jsonPath)
2428
{
2529
if (!IsValidPath(jsonPath))
2630
{
2731
throw new InvalidDataException($"'{jsonPath}' is not a valid '{nameof(jsonPath)}'");
2832
}
29-
33+
3034
JToken currentNode = _rootNode;
3135
foreach (var property in jsonPath.Split('.'))
3236
{
@@ -40,7 +44,17 @@ public bool Exists(string jsonPath)
4044
return currentNode != null;
4145
}
4246

43-
public void SetToken(string jsonPath, JToken token)
47+
/// <summary>
48+
/// This method converts the supplied token it into a <see cref="JToken"/> type and sets it at the dot(.) seperated jsonPath.
49+
/// Any non-existing nodes in the jsonPath are created on the fly.
50+
/// All non-terminal nodes in the jsonPath need to be of type <see cref="JObject"/>.
51+
/// </summary>
52+
/// <param name="jsonPath">dot(.) seperated path. Example "Person.Name.FirstName"</param>
53+
/// <param name="token">The object to set at the specified jsonPath</param>
54+
/// <param name="tokenType"><see cref="TokenType"/>This does not play any role while setting a token for the JsonWriter</param>
55+
/// <exception cref="InvalidDataException">Thrown if the jsonPath is invalid</exception>
56+
/// <exception cref="InvalidOperationException">Thrown if the terminal property in the jsonPath is null/empty or if any non-terminal nodes in the jsonPath cannot be converted to <see cref="JObject"/></exception>
57+
public void SetToken(string jsonPath, object token, TokenType tokenType = TokenType.Other)
4458
{
4559
if (!IsValidPath(jsonPath))
4660
{
@@ -50,17 +64,24 @@ public void SetToken(string jsonPath, JToken token)
5064
{
5165
return;
5266
}
53-
67+
5468
var pathList = jsonPath.Split('.');
5569
var lastProperty = pathList.LastOrDefault();
5670
if (string.IsNullOrEmpty((lastProperty)))
5771
{
5872
throw new InvalidOperationException($"Cannot set a token at '{jsonPath}' because the terminal property is null or empty");
5973
}
74+
75+
var terminalToken = GetDeserializedToken<JToken>(token);
6076
var currentNode = _rootNode;
6177

6278
for (var i = 0; i < pathList.Length-1; i++)
6379
{
80+
if (currentNode == null)
81+
{
82+
throw new InvalidOperationException($"Cannot set a token at '{jsonPath}' because one of the nodes in the path is null");
83+
}
84+
6485
var property = pathList[i];
6586
if (!currentNode.ContainsKey(property))
6687
{
@@ -73,10 +94,17 @@ public void SetToken(string jsonPath, JToken token)
7394
}
7495
}
7596

76-
currentNode[lastProperty] = token;
97+
currentNode[lastProperty] = terminalToken;
7798
}
7899

79-
public JToken GetToken(string jsonPath, JToken defaultToken = null)
100+
/// <summary>
101+
/// Gets the object stored at the dot(.) seperated jsonPath. If the path does not exist then return the defaultToken.
102+
/// The defaultToken is only returned if it holds a non-null value.
103+
/// </summary>
104+
/// <param name="jsonPath">dot(.) seperated path. Example "Person.Name.FirstName"</param>
105+
/// <param name="defaultToken">The object that is returned if jsonPath does not exist.</param>
106+
/// <exception cref="InvalidOperationException">Thrown if the jsonPath does not exist and the defaultToken is null</exception>
107+
public object GetToken(string jsonPath, object defaultToken = null)
80108
{
81109
if (!Exists(jsonPath))
82110
{
@@ -96,6 +124,30 @@ public JToken GetToken(string jsonPath, JToken defaultToken = null)
96124
return currentNode;
97125
}
98126

127+
/// <summary>
128+
/// Gets the object stored at the dot(.) seperated jsonPath. If the path does not exist then return the defaultToken.
129+
/// The defaultToken is only returned if it holds a non-null value.
130+
/// The object is deserialized into type T before being returned.
131+
/// </summary>
132+
/// <param name="jsonPath">dot(.) seperated path. Example "Person.Name.FirstName"</param>
133+
/// <param name="defaultToken">The object that is returned if jsonPath does not exist in the JSON blob. It will be convert to type T before being returned.</param>
134+
/// <exception cref="InvalidOperationException">Thrown if the jsonPath does not exist and the defaultToken is null</exception>
135+
public T GetToken<T>(string jsonPath, object defaultToken = null)
136+
{
137+
var token = GetToken(jsonPath, defaultToken);
138+
if (token == null)
139+
{
140+
throw new InvalidOperationException($"'{jsonPath}' points to a null token");
141+
}
142+
143+
return GetDeserializedToken<T>(token);
144+
}
145+
146+
/// <summary>
147+
/// Deletes the token found at the dot(.) separated jsonPath. It does not do anything if the jsonPath does not exist.
148+
/// </summary>
149+
/// <param name="jsonPath">dot(.) seperated path. Example "Person.Name.FirstName"</param>
150+
/// <exception cref="InvalidOperationException">Thrown if the terminal property in jsonPath is null or empty</exception>
99151
public void RemoveToken(string jsonPath)
100152
{
101153
if (!Exists(jsonPath))
@@ -105,7 +157,7 @@ public void RemoveToken(string jsonPath)
105157

106158
var pathList = jsonPath.Split('.');
107159
var lastProperty = pathList.LastOrDefault();
108-
if (string.IsNullOrEmpty((lastProperty)))
160+
if (string.IsNullOrEmpty(lastProperty))
109161
{
110162
throw new InvalidOperationException(
111163
$"Cannot remove the token at '{jsonPath}' because the terminal property is null or empty");
@@ -121,22 +173,43 @@ public void RemoveToken(string jsonPath)
121173
currentNode.Remove(lastProperty);
122174
}
123175

124-
public string GetPrettyJson()
176+
/// <summary>
177+
/// Returns the template as a string
178+
/// </summary>
179+
public string GetContent()
125180
{
126181
return JsonConvert.SerializeObject(_rootNode, formatting: Formatting.Indented);
127182
}
128183

184+
/// <summary>
185+
/// Converts the JSON string into a <see cref="JObject"/>
186+
/// </summary>
187+
/// <param name="content"></param>
129188
public void Parse(string content)
130189
{
131190
_rootNode = string.IsNullOrEmpty(content) ? new JObject() : JObject.Parse(content);
132191
}
133-
192+
193+
/// <summary>
194+
/// Validates that the jsonPath is not null or comprises only of white spaces. Also ensures that it does not have consecutive dots(.)
195+
/// </summary>
196+
/// <param name="jsonPath"></param>
197+
/// <returns>true if the path is valid, else fail</returns>
134198
private bool IsValidPath(string jsonPath)
135199
{
136200
if (string.IsNullOrWhiteSpace(jsonPath))
137201
return false;
138-
139-
return !jsonPath.Split('.').Any(x => string.IsNullOrWhiteSpace(x));
202+
203+
return !jsonPath.Split('.').Any(string.IsNullOrWhiteSpace);
204+
}
205+
206+
private T GetDeserializedToken<T>(object token)
207+
{
208+
if (token is T deserializedToken)
209+
{
210+
return deserializedToken;
211+
}
212+
return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(token));
140213
}
141214
}
142215
}

0 commit comments

Comments
 (0)