diff --git a/CodeSnippetsReflection.OpenAPI.Test/AssertExtensions.cs b/CodeSnippetsReflection.OpenAPI.Test/AssertExtensions.cs new file mode 100644 index 000000000..d45d4fae1 --- /dev/null +++ b/CodeSnippetsReflection.OpenAPI.Test/AssertExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace CodeSnippetsReflection.OpenAPI.Test +{ + public static class AssertExtensions + { + public static void ContainsIgnoreWhiteSpace(string expectedSubstring, string actualString) + { + Xunit.Assert.Contains( + expectedSubstring.Replace(" ", string.Empty).Replace(Environment.NewLine, string.Empty), + actualString.Replace(" ", string.Empty).Replace(Environment.NewLine, string.Empty) + ); + } + } +} diff --git a/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs b/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs new file mode 100644 index 000000000..1adf45e2f --- /dev/null +++ b/CodeSnippetsReflection.OpenAPI.Test/SnippetCodeGraphTests.cs @@ -0,0 +1,304 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using CodeSnippetsReflection.OpenAPI.ModelGraph; +using Microsoft.OpenApi.Services; +using Xunit; + +namespace CodeSnippetsReflection.OpenAPI.Test +{ + public class SnippetCodeGraphTests + { + private const string ServiceRootUrl = "https://graph.microsoft.com/v1.0"; + private static OpenApiUrlTreeNode _v1TreeNode; + + private static string TypesSample = @" + { + ""attendees"": [ + { + ""type"": ""null"", + ""emailAddress"": { + ""name"": ""Alex Wilbur"", + ""address"": ""alexw@contoso.onmicrosoft.com"" + } + } + ], + ""locationConstraint"": { + ""isRequired"": false, + ""suggestLocation"": false, + ""locations"": [ + { + ""resolveAvailability"": false, + ""displayName"": ""Conf room Hood"" + } + ] + }, + ""timeConstraint"": { + ""activityDomain"":""work"", + ""timeSlots"": [ + { + ""start"": { + ""dateTime"": ""2019-04-16T09:00:00"", + ""timeZone"": ""Pacific Standard Time"" + }, + ""end"": { + ""dateTime"": ""2019-04-18T17:00:00"", + ""timeZone"": ""Pacific Standard Time"" + } + } + ] + }, + ""isOrganizerOptional"": ""false"", + ""meetingDuration"": ""PT1H"", + ""returnSuggestionReasons"": ""true"", + ""minimumAttendeePercentage"": 100 + } + "; + + // read the file from disk + private static async Task GetV1TreeNode() + { + if (_v1TreeNode == null) + { + _v1TreeNode = await SnippetModelTests.GetTreeNode("https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/openapi/v1.0/openapi.yaml"); + } + return _v1TreeNode; + } + + [Fact] + public async Task ParsesHeaders() + { + using var request = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users"); + + request.Headers.Add("Host", "graph.microsoft.com"); + request.Headers.Add("Prefer", "outlook.timezone=\"Pacific Standard Time\""); + + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var result = new SnippetCodeGraph(snippetModel); + var header = result.Headers.First(); + + Assert.True(result.HasHeaders()); + Assert.Single(result.Headers); // host should be ignored in headers + Assert.Equal("outlook.timezone=\"Pacific Standard Time\"", header.Value); + Assert.Equal("Prefer", header.Name); + Assert.Equal(PropertyType.String, header.PropertyType); + } + + [Fact] + public async Task HasHeadersIsFalseWhenNoneIsInRequest() + { + using var request = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users"); + request.Headers.Add("Host", "graph.microsoft.com"); + + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var result = new SnippetCodeGraph(snippetModel); + + Assert.False(result.HasHeaders()); + } + + [Fact] + public async Task ParsesParameters() + { + using var request = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users/19:4b6bed8d24574f6a9e436813cb2617d8?$select=displayName,givenName,postalCode,identities"); + + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var result = new SnippetCodeGraph(snippetModel); + var parameter = result.Parameters.First(); + + Assert.True(result.HasParameters()); + Assert.Single(result.Parameters); + + var expectedProperty = new CodeProperty { Name = "select", Value = "displayName,givenName,postalCode,identities", PropertyType = PropertyType.String, Children = null }; + Assert.Equal(expectedProperty, parameter); + + Assert.Equal("displayName,givenName,postalCode,identities", parameter.Value); + Assert.Equal("select", parameter.Name); + Assert.Equal(PropertyType.String, parameter.PropertyType); + + Assert.True(result.HasParameters()); + } + + [Fact] + public async Task ParsesQueryParametersWithSpaces() + { + using var request = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/roleManagement/directory/roleAssignments?$filter=roleDefinitionId eq '62e90394-69f5-4237-9190-012177145e10'&$expand=principal"); + + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var result = new SnippetCodeGraph(snippetModel); + var parameter = result.Parameters.First(); + + Assert.True(result.HasParameters()); + Assert.Equal(2, result.Parameters.Count()); + + var expectedProperty1 = new CodeProperty { Name = "filter", Value = "roleDefinitionId eq '62e90394-69f5-4237-9190-012177145e10'", PropertyType = PropertyType.String, Children = null }; + Assert.Equal(expectedProperty1, result.Parameters.First()); + + var expectedProperty2 = new CodeProperty { Name = "expand", Value = "principal", PropertyType = PropertyType.String, Children = null }; + Assert.Equal(expectedProperty2, result.Parameters.Skip(1).First()); + + } + + [Fact] + public async Task HasParametersIsFalseWhenNoParameterExists() + { + using var request = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users/19:4b6bed8d24574f6a9e436813cb2617d8"); + + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var result = new SnippetCodeGraph(snippetModel); + + Assert.False(result.HasParameters()); + } + + [Fact] + public async Task ParsesBodyTypeBinary() + { + using var request = new HttpRequestMessage(HttpMethod.Put, $"{ServiceRootUrl}/applications/{{application-id}}/logo") + { + Content = new ByteArrayContent(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 }) + }; + request.Content.Headers.ContentType = new("application/octet-stream"); + + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var result = new SnippetCodeGraph(snippetModel); + + Assert.Equal(PropertyType.Binary, result.Body.PropertyType); + } + + [Fact] + public async Task ParsesBodyWithoutProperContentType() + { + + var sampleBody = @" + { + ""createdDateTime"": ""2019-02-04T19:58:15.511Z"" + } + "; + + using var request = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/teams/team-id/channels/19:4b6bed8d24574f6a9e436813cb2617d8@thread.tacv2/messages") + { + Content = new StringContent(sampleBody, Encoding.UTF8) // snippet missing content type + }; + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var result = new SnippetCodeGraph(snippetModel); + + var expectedObject = new CodeProperty { Name = "MessagesPostRequestBody", Value = null, PropertyType = PropertyType.Object, Children = new List() }; + + Assert.Equal(expectedObject.Name, result.Body.Name); + Assert.Equal(expectedObject.Value, result.Body.Value); + Assert.Equal(expectedObject.PropertyType, result.Body.PropertyType); + } + + private CodeProperty? FindPropertyInSnippet(CodeProperty codeProperty, string name) + { + if (codeProperty.Name == name) return codeProperty; + + if (codeProperty.Children.Any()) + { + foreach (var param in codeProperty.Children) + { + if(FindPropertyInSnippet(param, name) is CodeProperty result) return result; + } + } + + return null; + } + + [Fact] + public async Task ParsesBodyPropertyTypeString() + { + using var request = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/findMeetingTimes") + { + Content = new StringContent(TypesSample, Encoding.UTF8) + }; + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var snippetCodeGraph = new SnippetCodeGraph(snippetModel); + + // meetingDuration should be a string + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "meetingDuration").Value; + + Assert.Equal(PropertyType.String, property.PropertyType); + Assert.Equal("PT1H", property.Value); + } + + [Fact] + public async Task ParsesBodyPropertyTypeNumber() + { + using var request = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/findMeetingTimes") + { + Content = new StringContent(TypesSample, Encoding.UTF8) + }; + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var snippetCodeGraph = new SnippetCodeGraph(snippetModel); + + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "minimumAttendeePercentage").Value; + + Assert.Equal(PropertyType.Number, property.PropertyType); + Assert.Equal("100" , property.Value); + } + + [Fact] + public async Task ParsesBodyPropertyTypeBoolean() + { + using var request = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/findMeetingTimes") + { + Content = new StringContent(TypesSample, Encoding.UTF8) + }; + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var snippetCodeGraph = new SnippetCodeGraph(snippetModel); + + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "suggestLocation").Value; + + Assert.Equal(PropertyType.Boolean, property.PropertyType); + Assert.Equal("False", property.Value); + } + + [Fact] + public async Task ParsesBodyPropertyTypeObject() + { + using var request = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/findMeetingTimes") + { + Content = new StringContent(TypesSample, Encoding.UTF8) + }; + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var snippetCodeGraph = new SnippetCodeGraph(snippetModel); + + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "locationConstraint").Value; + + Assert.Equal(PropertyType.Object, property.PropertyType); + } + + [Fact] + public async Task ParsesBodyPropertyTypeArray() + { + using var request = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/findMeetingTimes") + { + Content = new StringContent(TypesSample, Encoding.UTF8) + }; + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var snippetCodeGraph = new SnippetCodeGraph(snippetModel); + + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "attendees").Value; + + Assert.Equal(PropertyType.Array, property.PropertyType); + } + + [Fact] + public async Task ParsesBodyPropertyTypeMap() + { + using var request = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/findMeetingTimes") + { + Content = new StringContent(TypesSample, Encoding.UTF8) + }; + var snippetModel = new SnippetModel(request, ServiceRootUrl, await GetV1TreeNode()); + var snippetCodeGraph = new SnippetCodeGraph(snippetModel); + + var property = FindPropertyInSnippet(snippetCodeGraph.Body, "additionalData").Value; + + Assert.Equal(PropertyType.Map, property.PropertyType); + } + } + +} diff --git a/CodeSnippetsReflection.OpenAPI.Test/SnippetModelTests.cs b/CodeSnippetsReflection.OpenAPI.Test/SnippetModelTests.cs index 70eb9c947..4c7b90603 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/SnippetModelTests.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/SnippetModelTests.cs @@ -96,4 +96,4 @@ public async Task GetsTheResponseSchema() Assert.NotEmpty(snippetModel.ResponseSchema.Properties); } } -} \ No newline at end of file +} diff --git a/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs b/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs index d6694ae76..363752e49 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/TypeScriptGeneratorTest.cs @@ -34,7 +34,17 @@ public async Task GeneratesTheCorrectFluentAPIPath() using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/me/messages"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains(".me.messages", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + + const result = async () => { + await graphServiceClient.me.messages.get(); + } + "; + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] @@ -47,16 +57,45 @@ public async Task GeneratesClassWithDefaultBodyWhenSchemaNotPresent() }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("const requestBody = new BatchRecordDecisionsRequestBody()", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : BatchRecordDecisionsPostRequestBody = { + decision : ""Approve"", + justification : ""All principals with access need continued access to the resource (Marketing Group) as all the principals are on the marketing team"", + resourceId : ""a5c51e59-3fcd-4a37-87a1-835c0c21488a"", + }; + + async () => { + await graphServiceClient.identityGovernance.accessReviews.definitionsById(""accessReviewScheduleDefinition-id"").instancesById(""accessReviewInstance-id"").batchRecordDecisions.post(requestBody); + } + "; + + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } + [Fact] public async Task GeneratesTheCorrectFluentAPIPathForIndexedCollections() { using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/me/messages/{{message-id}}"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains(".me.messagesById(\"message-id\")", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + + const result = async () => { + await graphServiceClient.me.messagesById(""message-id"").get(); + } + "; + + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } + [Fact] public async Task GeneratesTheSnippetInitializationStatement() { @@ -65,31 +104,58 @@ public async Task GeneratesTheSnippetInitializationStatement() var result = _generator.GenerateCodeSnippet(snippetModel); Assert.Contains("const graphServiceClient = GraphServiceClient.init({authProvider});", result); } + [Fact] public async Task GeneratesTheGetMethodCall() { using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/me/messages"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("get", result); - Assert.Contains("await", result); + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + + const result = async () => { + await graphServiceClient.me.messages.get(); + } + "; + + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } + [Fact] public async Task GeneratesThePostMethodCall() { using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/messages"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("post", result); + var expected = @" + const result = async () => { + await graphServiceClient.me.messages.post(); + } + "; + + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } + [Fact] public async Task GeneratesThePatchMethodCall() { using var requestPayload = new HttpRequestMessage(HttpMethod.Patch, $"{ServiceRootUrl}/me/messages/{{message-id}}"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("patch", result); + var expected = @" + const result = async () => { + await graphServiceClient.me.messagesById(""message-id"").patch(); + } + "; + + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } + [Fact] public async Task GeneratesThePutMethodCall() { @@ -98,36 +164,74 @@ public async Task GeneratesThePutMethodCall() var result = _generator.GenerateCodeSnippet(snippetModel); Assert.Contains("put", result); } + [Fact] public async Task GeneratesTheDeleteMethodCall() { using var requestPayload = new HttpRequestMessage(HttpMethod.Delete, $"{ServiceRootUrl}/me/messages/{{message-id}}"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("delete", result); - Assert.DoesNotContain("let result =", result); + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + + const result = async () => { + await graphServiceClient.me.messagesById(""message-id"").delete(); + } + "; + + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } - // code + [Fact] public async Task WritesTheRequestPayload() { - const string userJsonObject = "{\r\n \"accountEnabled\": true,\r\n " + - "\"displayName\": \"displayName-value\",\r\n " + - "\"mailNickname\": \"mailNickname-value\",\r\n " + - "\"userPrincipalName\": \"upn-value@tenant-value.onmicrosoft.com\",\r\n " + - " \"passwordProfile\" : {\r\n \"forceChangePasswordNextSignIn\": true,\r\n \"password\": \"password-value\"\r\n }\r\n}";//nested passwordProfile Object + var sampleJson = @" + { + ""accountEnabled"": true, + ""displayName"": ""displayName-value"", + ""mailNickname"": ""mailNickname-value"", + ""userPrincipalName"": ""upn-value@tenant-value.onmicrosoft.com"", + "" passwordProfile"": { + ""forceChangePasswordNextSignIn"": true, + ""password"": ""password-value"" + } + } + "; using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/users") { - Content = new StringContent(userJsonObject, Encoding.UTF8, "application/json") + Content = new StringContent(sampleJson, Encoding.UTF8, "application/json") }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("new User", result); - Assert.Contains("requestBody.accountEnabled = true;", result); - Assert.Contains("requestBody.passwordProfile = new PasswordProfile", result); - Assert.Contains("requestBody.displayName = \"displayName-value\"", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : User = { + accountEnabled : true, + displayName : ""displayName-value"", + mailNickname : ""mailNickname-value"", + userPrincipalName : ""upn-value@tenant-value.onmicrosoft.com"", + additionalData : { + passwordProfile : { + forceChangePasswordNextSignIn : true, + password : ""password-value"", + }, + }, + }; + + const result = async () => { + await graphServiceClient.users.post(requestBody); + } + "; + + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } + [Fact] public async Task WritesALongAndFindsAnAction() { @@ -140,21 +244,40 @@ public async Task WritesALongAndFindsAnAction() var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); Assert.Contains("10", result); - Assert.DoesNotContain("microsoft.graph", result); + Assert.DoesNotContain("microsoft.graph1", result); } + [Fact] public async Task WritesADouble() { - const string userJsonObject = "{\r\n \"minimumAttendeePercentage\": 10\r\n\r\n}"; + var sampleJson = @" + { + ""minimumAttendeePercentage"": 10 + } + "; using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/findMeetingTimes") { - Content = new StringContent(userJsonObject, Encoding.UTF8, "application/json") + Content = new StringContent(sampleJson, Encoding.UTF8, "application/json") }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("10", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : FindMeetingTimesPostRequestBody = { + minimumAttendeePercentage : 10, + }; + + const result = async () => { + await graphServiceClient.me.findMeetingTimes.post(requestBody); + } + "; + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } + [Fact] public async Task GeneratesABinaryPayload() { @@ -165,8 +288,20 @@ public async Task GeneratesABinaryPayload() requestPayload.Content.Headers.ContentType = new("application/octet-stream"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("new WebStream", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody = new ArrayBuffer(16); + + async () => { + await graphServiceClient.applicationsById(""application-id"").logo.put(requestBody); + } + "; + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } + [Fact] public async Task GeneratesABase64UrlPayload() { @@ -177,8 +312,23 @@ public async Task GeneratesABase64UrlPayload() }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("btoa", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : ChatMessageHostedContent = { + contentBytes : ""wiubviuwbegviwubiu"", + }; + + const result = async () => { + await graphServiceClient.chatsById(""chat-id"").messagesById(""chatMessage-id"").hostedContents.post(requestBody); + } + "; + + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } + [Fact] public async Task GeneratesADatePayload() { @@ -189,22 +339,60 @@ public async Task GeneratesADatePayload() }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("new Date(", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : Message = { + receivedDateTime : new Date(""2021-08-30T20:00:00:00Z""), + }; + + const result = async () => { + await graphServiceClient.me.messages.post(requestBody); + } + "; + + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } + [Fact] public async Task GeneratesAnArrayPayloadInAdditionalData() { - const string userJsonObject = "{\r\n \"members@odata.bind\": [\r\n \"https://graph.microsoft.com/v1.0/directoryObjects/{id}\",\r\n \"https://graph.microsoft.com/v1.0/directoryObjects/{id}\",\r\n \"https://graph.microsoft.com/v1.0/directoryObjects/{id}\"\r\n ]\r\n}"; + var samplePayload = @" + { + ""members@odata.bind"": [ + ""https://graph.microsoft.com/v1.0/directoryObjects/{id}"", + ""https://graph.microsoft.com/v1.0/directoryObjects/{id}"", + ""https://graph.microsoft.com/v1.0/directoryObjects/{id}"" + ] + } + "; + using var requestPayload = new HttpRequestMessage(HttpMethod.Patch, $"{ServiceRootUrl}/groups/{{group-id}}") { - Content = new StringContent(userJsonObject, Encoding.UTF8, "application/json") + Content = new StringContent(samplePayload, Encoding.UTF8, "application/json") }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("\"members@odata.bind\" : [", result); - Assert.Contains("requestBody.additionalData", result); - Assert.Contains("members", result); // property name hasn't been changed + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : Group = { + additionalData : { + ""members@odata.bind"" : [ + ""https://graph.microsoft.com/v1.0/directoryObjects/{id}"", + ""https://graph.microsoft.com/v1.0/directoryObjects/{id}"", + ""https://graph.microsoft.com/v1.0/directoryObjects/{id}"", + ], + }, + }; + "; + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } + [Fact] public async Task GeneratesAnArrayOfObjectsPayloadData() { @@ -215,38 +403,107 @@ public async Task GeneratesAnArrayOfObjectsPayloadData() }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("const extension = new Extension();", result); // property is initialized on its own - Assert.Contains("extension.additionalData = {", result); // additional data is initialized as a Record not mpa - Assert.Contains("requestBody.extensions = [", result); // property is added to list + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : Group = { + extensions : [ + { + additionalData : { + dealValue : 10000, + }, + }, + ], + additionalData : { + body : { + contentType : ""HTML"", + }, + }, + }; + + const result = async () => { + await graphServiceClient.groupsById(""group-id"").patch(requestBody); + } + "; + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } + [Fact] public async Task GeneratesSelectQueryParameters() { using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/me?$select=displayName,id"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("displayName", result); - Assert.Contains("let requestParameters = {", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const configuration = { + queryParameters : { + select: ""displayName,id"", + } + }; + + const result = async () => { + await graphServiceClient.me.get(configuration); + } + "; + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } + [Fact] public async Task GeneratesCountBooleanQueryParameters() { using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users?$count=true&$select=displayName,id"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("displayName", result); - Assert.DoesNotContain("\"true\"", result); - Assert.Contains("true", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const configuration = { + queryParameters : { + count: ""true"", + select: ""displayName,id"", + } + }; + + const result = async () => { + await graphServiceClient.users.get(configuration); + } + "; + + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } + [Fact] public async Task GeneratesSkipQueryParameters() { using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users?$skip=10"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.DoesNotContain("\"10\"", result); - Assert.Contains("10", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const configuration = { + queryParameters : { + skip: ""10"", + } + }; + + const result = async () => { + await graphServiceClient.users.get(configuration); + } + "; + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } + [Fact] public async Task GeneratesSelectExpandQueryParameters() { @@ -257,6 +514,7 @@ public async Task GeneratesSelectExpandQueryParameters() Assert.Contains("members($select=id,displayName)", result); Assert.DoesNotContain("select :", result); } + [Fact] public async Task GeneratesRequestHeaders() { @@ -264,52 +522,129 @@ public async Task GeneratesRequestHeaders() requestPayload.Headers.Add("ConsistencyLevel", "eventual"); var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("\"ConsistencyLevel\": \"eventual\",", result); - Assert.Contains("const headers = {", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const configuration = { + headers : { + ""ConsistencyLevel"": ""eventual"", + } + }; + + const result = async () => { + await graphServiceClient.groups.get(configuration); + } + "; + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } + [Fact] public async Task GenerateAdditionalData() { - const string chatObject = "{ \"createdDateTime\":\"2019-02-04T19:58:15.511Z\", " + - "\"from\":{ \"user\":{ \"id\":\"id-value\", \"displayName\":\"Joh Doe\", " + - " \"userIdentityType\":\"aadUser\" } }, \"body\":{ \"contentType\":\"html\", " + - " \"content\":\"Hello World\" }}"; + var samplePayload = @" + { + ""createdDateTime"": ""2019-02-04T19:58:15.511Z"", + ""from"": { + ""user"": { + ""id"": ""id-value"", + ""displayName"": ""Joh Doe"", + ""userIdentityType"": ""aadUser"" + } + }, + ""body"": { + ""contentType"": ""html"", + ""content"": ""Hello World"" + } + } + "; using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/teams/team-id/channels/19:4b6bed8d24574f6a9e436813cb2617d8@thread.tacv2/messages") { - Content = new StringContent(chatObject, Encoding.UTF8, "application/json") + Content = new StringContent(samplePayload, Encoding.UTF8, "application/json") }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("new ChatMessage", result); - Assert.Contains("requestBody.from.user.additionalData", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : ChatMessage = { + createdDateTime : new Date(""2019-02-04T19:58:15.511Z""), + from : { + user : { + id : ""id-value"", + displayName : ""Joh Doe"", + additionalData : { + ""userIdentityType"" : ""aadUser"", + }, + }, + }, + body : { + contentType : BodyType.Html, + content : ""Hello World"", + }, + }; + + const result = async () => { + await graphServiceClient.teamsById(""team-id"").channelsById(""channel-id"").messages.post(requestBody); + } + "; + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } [Fact] public async Task GeneratesEnumsWhenVariableIsEnum() { - const string userJsonObject = @"{ - ""displayName"": ""Test create"", - ""settings"": { - ""recurrence"": { - ""pattern"": { - ""type"": ""weekly"", - ""interval"": 1 - }, - ""range"": { - ""type"": ""noEnd"", - ""startDate"": ""2020-09-08T12:02:30.667Z"" - } - } - } -}"; + const string payloadJson = @"{ + ""displayName"": ""Test create"", + ""settings"": { + ""recurrence"": { + ""pattern"": { + ""type"": ""weekly"", + ""interval"": 1 + }, + ""range"": { + ""type"": ""noEnd"", + ""startDate"": ""2020-09-08T12:02:30.667Z"" + } + } + } + }"; using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/identityGovernance/accessReviews/definitions") { - Content = new StringContent(userJsonObject, Encoding.UTF8, "application/json") + Content = new StringContent(payloadJson, Encoding.UTF8, "application/json") }; var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1TreeNode()); var result = _generator.GenerateCodeSnippet(snippetModel); - Assert.Contains("requestBody.settings.recurrence.pattern.type = RecurrencePatternType.Weekly", result); + + var expected = @" + const graphServiceClient = GraphServiceClient.init({authProvider}); + + const requestBody : AccessReviewScheduleDefinition = { + displayName : ""Test create"", + settings : { + recurrence : { + pattern : { + type : RecurrencePatternType.Weekly, + interval : 1, + }, + range : { + type : RecurrenceRangeType.NoEnd, + startDate : ""2020-09-08T12:02:30.667Z"", + }, + }, + }, + }; + + const result = async () => { + await graphServiceClient.identityGovernance.accessReviews.definitions.post(requestBody); + } + "; + + AssertExtensions.ContainsIgnoreWhiteSpace(expected, result); } } } diff --git a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs index 347accb98..a7d14ede9 100644 --- a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs +++ b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/TypeScriptGenerator.cs @@ -5,6 +5,7 @@ using System.Text; using System.Text.Json; using System.Text.RegularExpressions; +using CodeSnippetsReflection.OpenAPI.ModelGraph; using CodeSnippetsReflection.StringExtensions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; @@ -18,304 +19,201 @@ public class TypeScriptGenerator : ILanguageGenerator /// Model of the Snippets info /// String of the snippet in Javascript code - /// + /// - private const string clientVarName = "graphServiceClient"; - private const string clientVarType = "GraphServiceClient"; + private const string ClientVarName = "graphServiceClient"; + private const string ClientVarType = "GraphServiceClient"; + private const string RequestHeadersVarName = "headers"; + private const string RequestOptionsVarName = "options"; + private const string RequestConfigurationVarName = "configuration"; + private const string RequestParametersVarName = "queryParameters"; + private const string RequestBodyVarName = "requestBody"; public string GenerateCodeSnippet(SnippetModel snippetModel) { if (snippetModel == null) throw new ArgumentNullException("Argument snippetModel cannot be null"); - var indentManager = new IndentManager(); + + var codeGraph = new SnippetCodeGraph(snippetModel); var snippetBuilder = new StringBuilder( "//THIS SNIPPET IS A PREVIEW FOR THE KIOTA BASED SDK. NON-PRODUCTION USE ONLY" + Environment.NewLine + - $"const {clientVarName} = {clientVarType}.init({{authProvider}});{Environment.NewLine}{Environment.NewLine}"); - var (requestPayload, payloadVarName) = GetRequestPayloadAndVariableName(snippetModel, indentManager); - snippetBuilder.Append(requestPayload); - var responseAssignment = snippetModel.ResponseSchema == null ? string.Empty : "const result = "; + $"const {ClientVarName} = {ClientVarType}.init({{authProvider}});{Environment.NewLine}{Environment.NewLine}"); - // add headers - var (requestHeadersPayload, requestHeadersVarName) = GetRequestHeaders(snippetModel, indentManager); - if (!string.IsNullOrEmpty(requestHeadersPayload)) snippetBuilder.Append(requestHeadersPayload); - - // add query parameters - var (queryParamsPayload, queryParamsVarName) = GetRequestQueryParameters(snippetModel, indentManager); - if (!string.IsNullOrEmpty(queryParamsPayload)) snippetBuilder.Append(queryParamsPayload); - - // add parameters - var parametersList = GetActionParametersList(payloadVarName, queryParamsVarName, requestHeadersVarName); - var methodName = snippetModel.Method.ToString().ToLower(); - snippetBuilder.AppendLine($"{responseAssignment}async () => {{"); - indentManager.Indent(); - snippetBuilder.AppendLine($"{indentManager.GetIndent()}await {clientVarName}.{GetFluentApiPath(snippetModel.PathNodes)}.{methodName}({parametersList});"); - indentManager.Unindent(); - snippetBuilder.AppendLine($"}}"); + writeSnippet(codeGraph, snippetBuilder); return snippetBuilder.ToString(); } - private const string RequestHeadersVarName = "headers"; - private static (string, string) GetRequestHeaders(SnippetModel snippetModel, IndentManager indentManager) - { - var payloadSB = new StringBuilder(); - var filteredHeaders = snippetModel.RequestHeaders.Where(h => !h.Key.Equals("Host", StringComparison.OrdinalIgnoreCase)) - .ToList(); - if (filteredHeaders.Any()) - { - payloadSB.AppendLine($"{indentManager.GetIndent()}const {RequestHeadersVarName} = {{"); - indentManager.Indent(); - filteredHeaders.ForEach(h => - payloadSB.AppendLine($"{indentManager.GetIndent()}\"{h.Key}\": \"{h.Value.FirstOrDefault().Replace("\"", "\\\"")}\",") - ); - indentManager.Unindent(); - payloadSB.AppendLine($"{indentManager.GetIndent()}}};"); - return (payloadSB.ToString(), RequestHeadersVarName); - } - return (default, default); - } - private static string GetActionParametersList(params string[] parameters) + + private static void writeSnippet(SnippetCodeGraph codeGraph, StringBuilder builder) { - var nonEmptyParameters = parameters.Where(p => !string.IsNullOrEmpty(p)); - if (nonEmptyParameters.Any()) - return string.Join(", ", nonEmptyParameters.Aggregate((a, b) => $"{a}, {b}")); - else return string.Empty; + writeHeadersAndOptions(codeGraph, builder); + WriteBody(codeGraph, builder); + builder.AppendLine(""); + + WriteExecutionStatement( + codeGraph, + builder, + codeGraph.HasBody() ? RequestBodyVarName : default, + codeGraph.HasHeaders() || codeGraph.HasOptions() || codeGraph.HasParameters() ? RequestConfigurationVarName : default + ); } - private const string RequestParametersVarName = "requestParameters"; - private static (string, string) GetRequestQueryParameters(SnippetModel model, IndentManager indentManager) + + private static void writeHeadersAndOptions(SnippetCodeGraph codeGraph, StringBuilder builder) { - var payloadSB = new StringBuilder(); - if (!string.IsNullOrEmpty(model.QueryString)) - { - payloadSB.AppendLine($"{indentManager.GetIndent()}let {RequestParametersVarName} = {{"); - indentManager.Indent(); - var (queryString, replacements) = ReplaceNestedOdataQueryParameters(model.QueryString); - foreach (var queryParam in queryString.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries)) - { - if (queryParam.Contains("=")) - { - var kvPair = queryParam.Split('=', StringSplitOptions.RemoveEmptyEntries); - payloadSB.AppendLine($"{indentManager.GetIndent()}{NormalizeQueryParameterName(kvPair[0])} : {GetQueryParameterValue(kvPair[1], replacements)},"); - } - else - payloadSB.AppendLine($"q.{indentManager.GetIndent()}{NormalizeQueryParameterName(queryParam)} = undefined;"); - } - indentManager.Unindent(); - payloadSB.AppendLine($"{indentManager.GetIndent()}}};"); - return (payloadSB.ToString(), RequestParametersVarName); - } - return (default, default); + if (!codeGraph.HasHeaders() && !codeGraph.HasOptions() && !codeGraph.HasParameters()) return; + + var indentManager = new IndentManager(); + builder.AppendLine($"const {RequestConfigurationVarName} = {{"); + indentManager.Indent(); + WriteHeader(codeGraph, builder, indentManager); + WriteOptions(codeGraph, builder, indentManager); + WriteParameters(codeGraph, builder, indentManager); + indentManager.Unindent(); + builder.AppendLine($"{indentManager.GetIndent()}}};"); } - private static Regex nestedStatementRegex = new(@"(\w+)(\([^)]+\))", RegexOptions.IgnoreCase|RegexOptions.Compiled); - private static (string, Dictionary) ReplaceNestedOdataQueryParameters(string queryParams) + + private static void WriteHeader(SnippetCodeGraph codeGraph, StringBuilder builder, IndentManager indentManager) { - var replacements = new Dictionary(); - var matches = nestedStatementRegex.Matches(queryParams); - if (matches.Any()) - foreach (Match match in matches) - { - var key = match.Groups[1].Value; - var value = match.Groups[2].Value; - replacements.Add(key, value); - queryParams = queryParams.Replace(value, string.Empty); - } - return (queryParams, replacements); + if (!codeGraph.HasHeaders()) return; + + builder.AppendLine($"{indentManager.GetIndent()}{RequestHeadersVarName} : {{"); + indentManager.Indent(); + foreach (var param in codeGraph.Headers) + builder.AppendLine($"{indentManager.GetIndent()}\"{param.Name}\": \"{param.Value.EscapeQuotes()}\","); + indentManager.Unindent(); + builder.AppendLine($"{indentManager.GetIndent()}}}"); } - private static string GetQueryParameterValue(string originalValue, Dictionary replacements) + private static void WriteOptions(SnippetCodeGraph codeGraph, StringBuilder builder, IndentManager indentManager) { - if (originalValue.Equals("true", StringComparison.OrdinalIgnoreCase) || originalValue.Equals("false", StringComparison.OrdinalIgnoreCase)) - return originalValue.ToLowerInvariant(); - else if (int.TryParse(originalValue, out var intValue)) - return intValue.ToString(); - else - { - var valueWithNested = originalValue.Split(',') - .Select(v => replacements.ContainsKey(v) ? v + replacements[v] : v) - .Aggregate((a, b) => $"{a},{b}"); - return $"\"{valueWithNested}\""; - } + if (!codeGraph.HasOptions()) return; + + if (codeGraph.HasHeaders()) + builder.Append(','); + + builder.AppendLine($"{indentManager.GetIndent()}{RequestOptionsVarName} : {{"); + indentManager.Indent(); + foreach (var param in codeGraph.Options) + builder.AppendLine($"{indentManager.GetIndent()}\"{param.Name}\": \"{param.Value.EscapeQuotes()}\","); + indentManager.Unindent(); + builder.AppendLine($"{indentManager.GetIndent()}}}"); } - private static string NormalizeQueryParameterName(string queryParam) => queryParam.TrimStart('$').ToFirstCharacterLowerCase(); - private const string RequestBodyVarName = "requestBody"; - private static (string, string) GetRequestPayloadAndVariableName(SnippetModel snippetModel, IndentManager indentManager) + + private static void WriteParameters(SnippetCodeGraph codeGraph, StringBuilder builder, IndentManager indentManager) { - if (string.IsNullOrWhiteSpace(snippetModel?.RequestBody)) - return (default, default); - if (indentManager == null) throw new ArgumentNullException(nameof(indentManager)); + if (!codeGraph.HasParameters()) return; - var payloadSB = new StringBuilder(); - switch (snippetModel.ContentType?.Split(';').First().ToLowerInvariant()) - { - case "application/json": - TryParseBody(snippetModel, payloadSB, indentManager); - break; - case "application/octet-stream": - payloadSB.AppendLine($"using var {RequestBodyVarName} = new WebStream();"); - break; - default: - if(TryParseBody(snippetModel, payloadSB, indentManager)) //in case the content type header is missing but we still have a json payload - break; - else - throw new InvalidOperationException($"Unsupported content type: {snippetModel.ContentType}"); - } - var result = payloadSB.ToString(); - return (result, string.IsNullOrEmpty(result) ? string.Empty : RequestBodyVarName); - } - private static bool TryParseBody(SnippetModel snippetModel, StringBuilder payloadSB, IndentManager indentManager) { - if(snippetModel.IsRequestBodyValid) - try { - using var parsedBody = JsonDocument.Parse(snippetModel.RequestBody, new JsonDocumentOptions { AllowTrailingCommas = true }); - var schema = snippetModel.RequestSchema; - var className = schema.GetSchemaTitle().ToFirstCharacterUpperCase() ?? $"{snippetModel.Path.Split("/").Last().ToFirstCharacterUpperCase()}RequestBody"; - payloadSB.AppendLine($"const {RequestBodyVarName} = new {className}();"); - WriteJsonObjectValue(RequestBodyVarName, payloadSB, parsedBody.RootElement, schema, indentManager); - return true; - } catch (Exception ex) when (ex is JsonException || ex is ArgumentException) { - // the payload wasn't json or poorly formatted - } - return false; + if (codeGraph.HasHeaders() || codeGraph.HasOptions()) + builder.Append(','); + + builder.AppendLine($"{indentManager.GetIndent()}{RequestParametersVarName} : {{"); + indentManager.Indent(); + foreach (var param in codeGraph.Parameters) + builder.AppendLine($"{indentManager.GetIndent()}{NormalizeJsonName(param.Name)}: \"{param.Value.EscapeQuotes()}\","); + indentManager.Unindent(); + builder.AppendLine($"{indentManager.GetIndent()}}}"); } - private static void WriteAnonymousObjectValues(StringBuilder payloadSB, JsonElement value, OpenApiSchema schema, IndentManager indentManager, bool includePropertyAssignment = true) + + private static void WriteExecutionStatement(SnippetCodeGraph codeGraph, StringBuilder builder, params string[] parameters) { - if (value.ValueKind != JsonValueKind.Object) throw new InvalidOperationException($"Expected JSON object and got {value.ValueKind}"); - indentManager.Indent(); + var methodName = codeGraph.HttpMethod.ToString().ToLower(); + var responseAssignment = codeGraph.ResponseSchema == null ? string.Empty : "const result = "; - var propertiesAndSchema = value.EnumerateObject() - .Select(x => new Tuple(x, schema.GetPropertySchema(x.Name))); - foreach (var propertyAndSchema in propertiesAndSchema) - { - var propertyName = propertyAndSchema.Item1.Name.ToFirstCharacterLowerCase(); - var propertyAssignment = includePropertyAssignment ? $"{indentManager.GetIndent()} [\"{propertyName}\" , " : string.Empty; - WriteProperty(string.Empty, payloadSB, propertyAndSchema.Item1.Value, propertyAndSchema.Item2, indentManager, propertyAssignment, "]", ","); - } + var parametersList = GetActionParametersList(parameters); + var indentManager = new IndentManager(); + builder.AppendLine($"{responseAssignment}async () => {{"); + indentManager.Indent(); + builder.AppendLine($"{indentManager.GetIndent()}await {ClientVarName}.{GetFluentApiPath(codeGraph.Nodes)}.{methodName}({parametersList});"); indentManager.Unindent(); + builder.AppendLine($"}}"); } - private static void WriteJsonObjectValue(String objectName, StringBuilder payloadSB, JsonElement value, OpenApiSchema schema, IndentManager indentManager, bool includePropertyAssignment = true) + + private static void WriteBody(SnippetCodeGraph codeGraph, StringBuilder builder) { - if (value.ValueKind != JsonValueKind.Object) throw new InvalidOperationException($"Expected JSON object and got {value.ValueKind}"); - indentManager.Indent(); - var propertiesAndSchema = value.EnumerateObject() - .Select(x => new Tuple(x, schema.GetPropertySchema(x.Name))); - foreach (var propertyAndSchema in propertiesAndSchema.Where(x => x.Item2 != null)) + if (codeGraph.Body.PropertyType == PropertyType.Default) return; + + var indentManager = new IndentManager(); + + if (codeGraph.Body.PropertyType == PropertyType.Binary) { - var propertyName = propertyAndSchema.Item1.Name.ToFirstCharacterLowerCase(); - var propertyAssignment = includePropertyAssignment ? $"{objectName}.{propertyName} = " : string.Empty; - WriteProperty($"{objectName}.{propertyName}", payloadSB, propertyAndSchema.Item1.Value, propertyAndSchema.Item2, indentManager, propertyAssignment); + builder.AppendLine($"{indentManager.GetIndent()}const {RequestBodyVarName} = new ArrayBuffer({codeGraph.Body.Value.Length});"); } - var propertiesWithoutSchema = propertiesAndSchema.Where(x => x.Item2 == null).Select(x => x.Item1); - if (propertiesWithoutSchema.Any()) + else { - payloadSB.AppendLine($"{objectName}.additionalData = {{"); + builder.AppendLine($"{indentManager.GetIndent()}const {RequestBodyVarName} : {codeGraph.Body.Name} = {{"); indentManager.Indent(); - - int elementIndex = 0; - var lastIndex = propertiesWithoutSchema.Count() - 1; - foreach (var property in propertiesWithoutSchema) - { - var propertyAssignment = $"{indentManager.GetIndent()} \"{property.Name}\" : "; - WriteProperty(objectName, payloadSB, property.Value, null, indentManager, propertyAssignment, "", (lastIndex == elementIndex) ? default : ","); - elementIndex++; - } + WriteCodePropertyObject(builder, codeGraph.Body, indentManager); indentManager.Unindent(); - payloadSB.AppendLine($"{indentManager.GetIndent()} }}"); + builder.AppendLine($"}};"); } - indentManager.Unindent(); } - private static void WriteProperty(String objectName, StringBuilder payloadSB, JsonElement value, OpenApiSchema propSchema, IndentManager indentManager, string propertyAssignment, string propertySuffix = default, string terminateLine = ";", bool objectPropertyAssign = true) - { - switch (value.ValueKind) - { - case JsonValueKind.String: - if (propSchema?.Format?.Equals("base64url", StringComparison.OrdinalIgnoreCase) ?? false) - payloadSB.AppendLine($"{propertyAssignment}btoa(\"{value.GetString()}\"){propertySuffix}{terminateLine}"); - else if (propSchema?.Format?.Equals("date-time", StringComparison.OrdinalIgnoreCase) ?? false) - payloadSB.AppendLine($"{propertyAssignment} new Date(\"{value.GetString()}\"){propertySuffix}{terminateLine}"); - else - { - var enumSchema = propSchema?.AnyOf.FirstOrDefault(x => x.Enum.Count > 0); - if(enumSchema == null) - { - payloadSB.AppendLine($"{propertyAssignment}\"{value.GetString()}\"{propertySuffix}{terminateLine}"); - } - else - { - payloadSB.AppendLine($"{propertyAssignment}{enumSchema.Title.ToFirstCharacterUpperCase()}.{value.GetString().ToFirstCharacterUpperCase()}{propertySuffix}{terminateLine}"); - } - } - break; - case JsonValueKind.Number: - payloadSB.AppendLine($"{propertyAssignment}{value}{propertySuffix}{terminateLine}"); - break; - case JsonValueKind.False: - case JsonValueKind.True: - payloadSB.AppendLine($"{propertyAssignment}{value.GetBoolean().ToString().ToLowerInvariant()}{propertySuffix}{terminateLine}"); - break; - case JsonValueKind.Null: - payloadSB.AppendLine($"{propertyAssignment}null{propertySuffix},"); - break; - case JsonValueKind.Object: - if (propSchema != null) - { - if(objectPropertyAssign) - payloadSB.AppendLine($"{propertyAssignment}new {propSchema.GetSchemaTitle().ToFirstCharacterUpperCase()}(){terminateLine}"); - - WriteJsonObjectValue(objectName, payloadSB, value, propSchema, indentManager); - } - else - { - WriteAnonymousObjectValues(payloadSB, value, propSchema, indentManager); - } - break; - case JsonValueKind.Array: - WriteJsonArrayValue(objectName, payloadSB, value, propSchema, indentManager, propertyAssignment, "],"); - break; - default: - throw new NotImplementedException($"Unsupported JsonValueKind: {value.ValueKind}"); - } + private static string NormalizeJsonName(string Name) + { + return (!String.IsNullOrWhiteSpace(Name) && Name.Substring(1) != "\"") && (Name.Contains('.') || Name.Contains('-')) ? $"\"{Name}\"" : Name; } - private static void WriteJsonArrayValue(String objectName, StringBuilder payloadSB, JsonElement value, OpenApiSchema schema, IndentManager indentManager, string propertyAssignment, string terminateLine) + + private static void WriteCodePropertyObject(StringBuilder builder, CodeProperty codeProperty, IndentManager indentManager) { - indentManager.Indent(2); - - // for an array of objects the properties should be written after all the objects have be initiated and not before - var itemsBuilder = new StringBuilder(); - var arrayListBuilder = new StringBuilder(); - - int elementIndex = 0; - var elements = value.EnumerateArray(); - var lastIndex = elements.Count() - 1; - foreach (var item in elements) + var isArray = codeProperty.PropertyType == PropertyType.Array; + foreach (var child in codeProperty.Children) { - if (item.ValueKind == JsonValueKind.Object) + switch (child.PropertyType) { - var termination = (lastIndex == elementIndex) ? default : ","; - - var elementName = $"{schema.GetSchemaTitle().ToLowerInvariant()}{(elementIndex == 0 ? default : elementIndex.ToString())}"; - itemsBuilder.AppendLine($"const {elementName} = new {schema.GetSchemaTitle().ToFirstCharacterUpperCase()}();"); - - // create a new object - WriteProperty(elementName, itemsBuilder, item, schema, indentManager, default, default, termination, false); - arrayListBuilder.AppendLine($"{indentManager.GetIndent()}{elementName}{termination}"); - } - else - { - WriteProperty(objectName, arrayListBuilder, item, schema, indentManager, indentManager.GetIndent(), default, (lastIndex == elementIndex) ? default : ","); - } - elementIndex++; - } - + case PropertyType.Object: + case PropertyType.Map: + if (isArray) + builder.AppendLine($"{indentManager.GetIndent()}{{"); + else + builder.AppendLine($"{indentManager.GetIndent()}{NormalizeJsonName(child.Name.ToFirstCharacterLowerCase())} : {{"); - payloadSB.Append(itemsBuilder.ToString()); + indentManager.Indent(); + WriteCodePropertyObject(builder, child, indentManager); + indentManager.Unindent(); + builder.AppendLine($"{indentManager.GetIndent()}}},"); - payloadSB.AppendLine($"{propertyAssignment}["); - payloadSB.Append(arrayListBuilder.ToString()); - indentManager.Unindent(); - payloadSB.AppendLine($"{indentManager.GetIndent()}]"); - indentManager.Unindent(); + break; + case PropertyType.Array: + builder.AppendLine($"{indentManager.GetIndent()}{NormalizeJsonName(child.Name)} : ["); + indentManager.Indent(); + WriteCodePropertyObject(builder, child, indentManager); + indentManager.Unindent(); + builder.AppendLine($"{indentManager.GetIndent()}],"); + break; + case PropertyType.String: + var propName = codeProperty.PropertyType == PropertyType.Map ? $"\"{child.Name.ToFirstCharacterLowerCase()}\"" : NormalizeJsonName(child.Name.ToFirstCharacterLowerCase()); + if (isArray || String.IsNullOrWhiteSpace(propName)) + builder.AppendLine($"{indentManager.GetIndent()}\"{child.Value}\","); + else + builder.AppendLine($"{indentManager.GetIndent()}{propName} : \"{child.Value}\","); + break; + case PropertyType.Enum: + if (!String.IsNullOrWhiteSpace(child.Value)) { + builder.AppendLine($"{indentManager.GetIndent()}{NormalizeJsonName(child.Name.ToFirstCharacterLowerCase())} : {child.Value},"); + } + break; + case PropertyType.Date: + builder.AppendLine($"{indentManager.GetIndent()}{NormalizeJsonName(child.Name)} : new Date(\"{child.Value}\"),"); + break; + case PropertyType.Base64Url: + builder.AppendLine($"{indentManager.GetIndent()}{NormalizeJsonName(child.Name.ToFirstCharacterLowerCase())} : \"{child.Value.ToFirstCharacterLowerCase()}\","); + break; + default: + builder.AppendLine($"{indentManager.GetIndent()}{NormalizeJsonName(child.Name.ToFirstCharacterLowerCase())} : {child.Value.ToFirstCharacterLowerCase()},"); + break; + } + } + } + private static string GetActionParametersList(params string[] parameters) + { + var nonEmptyParameters = parameters.Where(p => !string.IsNullOrEmpty(p)); + if (nonEmptyParameters.Any()) + return string.Join(", ", nonEmptyParameters.Aggregate((a, b) => $"{a}, {b}")); + else return string.Empty; } + private static string GetFluentApiPath(IEnumerable nodes) { if (!(nodes?.Any() ?? false)) return string.Empty; diff --git a/CodeSnippetsReflection.OpenAPI/ModelGraph/CodeProperty.cs b/CodeSnippetsReflection.OpenAPI/ModelGraph/CodeProperty.cs new file mode 100644 index 000000000..12bf9dd3f --- /dev/null +++ b/CodeSnippetsReflection.OpenAPI/ModelGraph/CodeProperty.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CodeSnippetsReflection.OpenAPI.ModelGraph +{ + public record struct CodeProperty(string Name, string Value, List Children, PropertyType PropertyType = PropertyType.String); + +} diff --git a/CodeSnippetsReflection.OpenAPI/ModelGraph/PropertyType.cs b/CodeSnippetsReflection.OpenAPI/ModelGraph/PropertyType.cs new file mode 100644 index 000000000..b411c255c --- /dev/null +++ b/CodeSnippetsReflection.OpenAPI/ModelGraph/PropertyType.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CodeSnippetsReflection.OpenAPI.ModelGraph +{ + public enum PropertyType + { + // Empty object + Default, + String , + Number, + Date , + Boolean , + Null, + Enum , + Object, + Base64Url, + Binary, + Array, + Map + } +} diff --git a/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs b/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs new file mode 100644 index 000000000..0233a00b4 --- /dev/null +++ b/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Web; +using CodeSnippetsReflection.StringExtensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; + +namespace CodeSnippetsReflection.OpenAPI.ModelGraph +{ + public record SnippetCodeGraph + { + + private static readonly Regex nestedStatementRegex = new(@"(\w+)(\([^)]+\))", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly CodeProperty EMPTY_PROPERTY = new() { Name = null, Value = null, Children = null, PropertyType = PropertyType.Default }; + + public SnippetCodeGraph(SnippetModel snippetModel) + { + ResponseSchema = snippetModel.ResponseSchema; + HttpMethod = snippetModel.Method; + Nodes = snippetModel.PathNodes; + Headers = parseHeaders(snippetModel); + Options = Enumerable.Empty(); + Parameters = parseParameters(snippetModel); + Body = parseBody(snippetModel); + } + + public OpenApiSchema ResponseSchema + { + get; set; + } + public HttpMethod HttpMethod + { + get; set; + } + + public IEnumerable Headers + { + get; set; + } + public IEnumerable Options + { + get; set; + } + + public IEnumerable Parameters + { + get; set; + } + + public CodeProperty Body + { + get; set; + } + + public IEnumerable Nodes + { + get; set; + } + + + public Boolean HasHeaders() + { + return Headers.Any(); + } + + public Boolean HasOptions() + { + return Options.Any(); + } + + public Boolean HasParameters() + { + return Parameters.Any(); + } + + public Boolean HasBody() + { + return Body.PropertyType != PropertyType.Default; + } + + + /// + /// Parses Headers Filtering Out 'Host' + /// + private static IEnumerable parseHeaders(SnippetModel snippetModel) + { + return snippetModel.RequestHeaders.Where(h => !h.Key.Equals("Host", StringComparison.OrdinalIgnoreCase)) + .Select(h => new CodeProperty { Name = h.Key, Value = h.Value?.FirstOrDefault(), Children = null, PropertyType = PropertyType.String }) + .ToList(); + } + + + private static List parseParameters(SnippetModel snippetModel) + { + var parameters = new List(); + if (!string.IsNullOrEmpty(snippetModel.QueryString)) + { + var (queryString, replacements) = ReplaceNestedOdataQueryParameters(snippetModel.QueryString); + + NameValueCollection queryCollection = HttpUtility.ParseQueryString(queryString); + foreach (String key in queryCollection.AllKeys) + { + parameters.Add(new() { Name = NormalizeQueryParameterName(key), Value = GetQueryParameterValue(queryCollection[key], replacements), PropertyType = PropertyType.String }); + } + + } + return parameters; + } + + private static string NormalizeQueryParameterName(string queryParam) => System.Web.HttpUtility.UrlDecode(queryParam.TrimStart('$').ToFirstCharacterLowerCase()); + + private static (string, Dictionary) ReplaceNestedOdataQueryParameters(string queryParams) + { + var replacements = new Dictionary(); + var matches = nestedStatementRegex.Matches(queryParams); + if (matches.Any()) + foreach (var groups in matches.Select(m => m.Groups)) + { + var key = groups[1].Value; + var value = groups[2].Value; + replacements.Add(key, value); + queryParams = queryParams.Replace(value, string.Empty); + } + return (queryParams, replacements); + } + + private static string GetQueryParameterValue(string originalValue, Dictionary replacements) + { + var escapedParam = System.Web.HttpUtility.UrlDecode(originalValue); + if (escapedParam.Equals("true", StringComparison.OrdinalIgnoreCase) || escapedParam.Equals("false", StringComparison.OrdinalIgnoreCase)) + return escapedParam.ToLowerInvariant(); + else if (int.TryParse(escapedParam, out var intValue)) + return intValue.ToString(); + else + { + return escapedParam.Split(',') + .Select(v => replacements.ContainsKey(v) ? v + replacements[v] : v) + .Aggregate((a, b) => $"{a},{b}"); + } + } + + private static CodeProperty parseBody(SnippetModel snippetModel) + { + if (string.IsNullOrWhiteSpace(snippetModel?.RequestBody)) + return EMPTY_PROPERTY; + + switch (snippetModel.ContentType?.Split(';').First().ToLowerInvariant()) + { + case "application/json": + return TryParseBody(snippetModel); + case "application/octet-stream": + return new() { Name = null, Value = snippetModel.RequestBody?.ToString(), Children = null, PropertyType = PropertyType.Binary }; + default: + return TryParseBody(snippetModel);//in case the content type header is missing but we still have a json payload + } + } + + private static string ComputeRequestBody(SnippetModel snippetModel) + { + var nodes = snippetModel.PathNodes; + if (!(nodes?.Any() ?? false)) return string.Empty; + + var nodeName = nodes.Where(x => !x.Segment.IsCollectionIndex()) + .Select(x => + { + if (x.Segment.IsFunction()) + return x.Segment.Split('.').Last(); + else + return x.Segment; + }) + .Last() + .ToFirstCharacterUpperCase(); + + var singularNodeName = nodeName[nodeName.Length - 1] == 's' ? nodeName.Substring(0, nodeName.Length - 1) : nodeName; + + if (nodes.Last()?.Segment?.IsCollectionIndex() == true) + return singularNodeName; + else + return $"{nodeName}PostRequestBody"; + + } + + private static CodeProperty TryParseBody(SnippetModel snippetModel) + { + if (!snippetModel.IsRequestBodyValid) + throw new InvalidOperationException($"Unsupported content type: {snippetModel.ContentType}"); + + using var parsedBody = JsonDocument.Parse(snippetModel.RequestBody, new JsonDocumentOptions { AllowTrailingCommas = true }); + var schema = snippetModel.RequestSchema; + var className = schema.GetSchemaTitle().ToFirstCharacterUpperCase() ?? ComputeRequestBody(snippetModel); + return parseJsonObjectValue(className, parsedBody.RootElement, schema); + } + + private static CodeProperty parseJsonObjectValue(String rootPropertyName, JsonElement value, OpenApiSchema schema) + { + var children = new List(); + + if (value.ValueKind != JsonValueKind.Object) throw new InvalidOperationException($"Expected JSON object and got {value.ValueKind}"); + + var propertiesAndSchema = value.EnumerateObject() + .Select(x => new Tuple(x, schema.GetPropertySchema(x.Name))); + foreach (var propertyAndSchema in propertiesAndSchema.Where(x => x.Item2 != null)) + { + var propertyName = propertyAndSchema.Item1.Name.ToFirstCharacterLowerCase(); + children.Add(parseProperty(propertyName, propertyAndSchema.Item1.Value, propertyAndSchema.Item2)); + } + + var propertiesWithoutSchema = propertiesAndSchema.Where(x => x.Item2 == null).Select(x => x.Item1); + if (propertiesWithoutSchema.Any()) + { + + var additionalChildren = new List(); + foreach (var property in propertiesWithoutSchema) + additionalChildren.Add(parseProperty(property.Name, property.Value, null)); + + if (additionalChildren.Any()) + children.Add(new CodeProperty { Name = "additionalData", PropertyType = PropertyType.Map, Children = additionalChildren }); + } + + return new CodeProperty { Name = rootPropertyName, PropertyType = PropertyType.Object, Children = children }; + } + + private static String escapeSpecialCharacters(string value) + { + return value?.Replace("\"", "\\\"")?.Replace("\n", "\\n")?.Replace("\r", "\\r"); + } + + private static CodeProperty evaluateStringProperty(string propertyName, JsonElement value, OpenApiSchema propSchema) + { + if (propSchema?.Format?.Equals("base64url", StringComparison.OrdinalIgnoreCase) ?? false) + return new CodeProperty { Name = propertyName, Value = value.GetString(), PropertyType = PropertyType.Base64Url, Children = new List() }; + + if (propSchema?.Format?.Equals("date-time", StringComparison.OrdinalIgnoreCase) ?? false) + return new CodeProperty { Name = propertyName, Value = value.GetString(), PropertyType = PropertyType.Date, Children = new List() }; + + + var enumSchema = propSchema?.AnyOf.FirstOrDefault(x => x.Enum.Count > 0); + if (enumSchema == null) + return new CodeProperty { Name = propertyName, Value = escapeSpecialCharacters(value.GetString()), PropertyType = PropertyType.String, Children = new List() }; + + + var propValue = String.IsNullOrWhiteSpace(value.GetString()) ? null : $"{enumSchema.Title.ToFirstCharacterUpperCase()}.{value.GetString().ToFirstCharacterUpperCase()}"; + return new CodeProperty { Name = propertyName, Value = propValue, PropertyType = PropertyType.Enum, Children = new List() }; + } + + private static CodeProperty parseProperty(string propertyName, JsonElement value, OpenApiSchema propSchema) + { + switch (value.ValueKind) + { + case JsonValueKind.String: + return evaluateStringProperty(propertyName, value, propSchema); + case JsonValueKind.Number: + return new CodeProperty { Name = propertyName, Value = $"{value}", PropertyType = PropertyType.Number, Children = new List() }; + case JsonValueKind.False: + case JsonValueKind.True: + return new CodeProperty { Name = propertyName, Value = value.GetBoolean().ToString(), PropertyType = PropertyType.Boolean, Children = new List() }; + case JsonValueKind.Null: + return new CodeProperty { Name = propertyName, Value = "null", PropertyType = PropertyType.Null, Children = new List() }; + case JsonValueKind.Object: + if (propSchema != null) + return parseJsonObjectValue(propertyName, value, propSchema); + else + return parseAnonymousObjectValues(propertyName, value, propSchema); + case JsonValueKind.Array: + return parseJsonArrayValue(propertyName, value, propSchema); + default: + throw new NotImplementedException($"Unsupported JsonValueKind: {value.ValueKind}"); + } + } + + private static CodeProperty parseJsonArrayValue(string propertyName, JsonElement value, OpenApiSchema schema) + { + var children = value.EnumerateArray().Select(item => parseProperty(schema.GetSchemaTitle().ToFirstCharacterUpperCase(), item, schema)).ToList(); + return new CodeProperty { Name = propertyName, Value = null, PropertyType = PropertyType.Array, Children = children }; + } + + private static CodeProperty parseAnonymousObjectValues(string propertyName, JsonElement value, OpenApiSchema schema) + { + if (value.ValueKind != JsonValueKind.Object) throw new InvalidOperationException($"Expected JSON object and got {value.ValueKind}"); + + var children = new List(); + var propertiesAndSchema = value.EnumerateObject() + .Select(x => new Tuple(x, schema.GetPropertySchema(x.Name))); + foreach (var propertyAndSchema in propertiesAndSchema) + { + children.Add(parseProperty(propertyAndSchema.Item1.Name.ToFirstCharacterLowerCase(), propertyAndSchema.Item1.Value, propertyAndSchema.Item2)); + } + + return new CodeProperty { Name = propertyName, Value = null, PropertyType = PropertyType.Object, Children = children }; + } + } + +} diff --git a/CodeSnippetsReflection.OpenAPI/OpenApiSchemaExtensions.cs b/CodeSnippetsReflection.OpenAPI/OpenApiSchemaExtensions.cs index 81994ff1b..21f248a84 100644 --- a/CodeSnippetsReflection.OpenAPI/OpenApiSchemaExtensions.cs +++ b/CodeSnippetsReflection.OpenAPI/OpenApiSchemaExtensions.cs @@ -13,7 +13,9 @@ public static IEnumerable> GetAllProperties( return schema.Properties .Union(schema.AllOf.FlattenEmptyEntries(x => x.AllOf, 2).SelectMany(x => x.Properties)) .Union(schema.AnyOf.SelectMany(x => x.Properties)) - .Union(schema.OneOf.SelectMany(x => x.Properties)); + .Union(schema.OneOf.SelectMany(x => x.Properties)) + .Union(schema.Items != null ? schema.Items.AllOf.SelectMany(x => x.Properties) : Enumerable.Empty>()) + .Union(schema.Items != null ? schema.Items.AnyOf.SelectMany(x => x.Properties) : Enumerable.Empty>()); } return schema.AllOf.Union(schema.AnyOf).Union(schema.OneOf).SelectMany(x => x.GetAllProperties()); } diff --git a/CodeSnippetsReflection/StringExtensions/StringExtensions.cs b/CodeSnippetsReflection/StringExtensions/StringExtensions.cs index dbec33690..3f8e1a9dd 100644 --- a/CodeSnippetsReflection/StringExtensions/StringExtensions.cs +++ b/CodeSnippetsReflection/StringExtensions/StringExtensions.cs @@ -34,5 +34,10 @@ public static string ToFirstCharacterUpperCaseAfterCharacter(this string stringV if (charIndex < 0) return stringValue; return stringValue[0..charIndex] + char.ToUpper(stringValue[charIndex + 1]) + stringValue[(charIndex + 2)..].ToFirstCharacterUpperCaseAfterCharacter(character); } + + public static string EscapeQuotes(this string stringValue) + { + return stringValue.Replace("\"", "\\\""); + } } }