diff --git a/CodeSnippetsReflection.App/Program.cs b/CodeSnippetsReflection.App/Program.cs index 4fa8425a4..a18b18ee1 100644 --- a/CodeSnippetsReflection.App/Program.cs +++ b/CodeSnippetsReflection.App/Program.cs @@ -96,7 +96,10 @@ static void Main(string[] args) var snippetGenerators = new ConcurrentDictionary(); Parallel.ForEach(supportedLanguages, language => { - generation = OpenApiSnippetsGenerator.SupportedLanguages.Contains(language) ? "openapi" : originalGeneration; + //Generation will still be originalGeneration if language is java since it is not stable + //Remove the condition when java is stable + generation = (OpenApiSnippetsGenerator.SupportedLanguages.Contains(language)) ? "openapi" : originalGeneration; + var generator = snippetGenerators.GetOrAdd(generation, generationKey => GetSnippetsGenerator(generationKey, customMetadataPathArg)); Parallel.ForEach(files, file => { diff --git a/CodeSnippetsReflection.OpenAPI.Test/CSharpGeneratorTests.cs b/CodeSnippetsReflection.OpenAPI.Test/CSharpGeneratorTests.cs index 6f2894a44..25aa438a3 100644 --- a/CodeSnippetsReflection.OpenAPI.Test/CSharpGeneratorTests.cs +++ b/CodeSnippetsReflection.OpenAPI.Test/CSharpGeneratorTests.cs @@ -1,4 +1,4 @@ -using System.Net.Http; +using System.Net.Http; using System.Text; using System.Threading.Tasks; using CodeSnippetsReflection.OpenAPI.LanguageGenerators; diff --git a/CodeSnippetsReflection.OpenAPI.Test/JavaGeneratorTests.cs b/CodeSnippetsReflection.OpenAPI.Test/JavaGeneratorTests.cs new file mode 100644 index 000000000..9b996bc6c --- /dev/null +++ b/CodeSnippetsReflection.OpenAPI.Test/JavaGeneratorTests.cs @@ -0,0 +1,937 @@ +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using CodeSnippetsReflection.OpenAPI.LanguageGenerators; +using Xunit; + +namespace CodeSnippetsReflection.OpenAPI.Test +{ + public class JavaGeneratorTests : OpenApiSnippetGeneratorTestBase + { + private readonly JavaGenerator _generator = new(); + + [Fact] + public async Task GeneratesTheCorrectFluentApiPath() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/me/messages"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains(".me().messages()", 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 GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains(".me().messages().byMessageId(\"{message-id}\")", result); + } + + [Fact] + public async Task GeneratesTheSnippetHeader() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/me/messages"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("GraphServiceClient graphClient = new GraphServiceClient(requestAdapter)", result); + } + + [Fact] + public async Task GeneratesTheGetMethodCall() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/me/messages"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("get()", result); + } + + [Fact] + public async Task GeneratesThePostMethodCall() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/messages"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("post(", 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 GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("patch(", result); + } + + [Fact] + public async Task GeneratesThePutMethodCall() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Put, $"{ServiceRootUrl}/applications/{{application-id}}/logo"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + 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 GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("delete(", result); + Assert.DoesNotContain("result =", result); + } + + [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 + + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/users") + { + Content = new StringContent(userJsonObject, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("new User()", result); + Assert.Contains("setAccountEnabled(true);", result); + Assert.Contains("setPasswordProfile(passwordProfile);", result); + Assert.Contains("setDisplayName(\"displayName-value\");", result); + } + + [Fact] + public async Task WritesALongAndFindsAnAction() + { + const string userJsonObject = "{\r\n \"chainId\": 10\r\n\r\n}"; + + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/teams/{{team-id}}/sendActivityNotification") + { + Content = new StringContent(userJsonObject, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("10L", result); + Assert.Contains("setChainId", result); + Assert.Contains("sendActivityNotificationPostRequestBody", result); + } + + [Fact] + public async Task WritesADouble() + { + const string userJsonObject = "{\r\n \"minimumAttendeePercentage\": 10\r\n\r\n}"; + + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/findMeetingTimes") + { + Content = new StringContent(userJsonObject, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("10d", result); + Assert.Contains("setMinimumAttendeePercentage", result); + } + + [Fact] + public async Task GeneratesABinaryPayload() + { + using var requestPayload = 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 }) + }; + requestPayload.Content.Headers.ContentType = new("application/octet-stream"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("new ByteArrayInputStream", result); + } + + [Fact] + public async Task GeneratesABase64UrlPayload() + { + const string userJsonObject = "{\r\n \"contentBytes\": \"wiubviuwbegviwubiu\"\r\n\r\n}"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/chats/{{chat-id}}/messages/{{chatMessage-id}}/hostedContents") + { + Content = new StringContent(userJsonObject, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("Base64.getDecoder().decode", result); + Assert.Contains("setContentBytes", result); + } + + [Fact] + public async Task GeneratesADateTimeOffsetPayload() + { + const string userJsonObject = "{\r\n \"receivedDateTime\": \"2021-08-30T20:00:00:00Z\"\r\n\r\n}"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/messages") + { + Content = new StringContent(userJsonObject, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("OffsetDateTime.parse(", result); + Assert.Contains("message.setReceivedDateTime(receivedDateTime);", 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}"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Patch, $"{ServiceRootUrl}/groups/{{group-id}}") + { + Content = new StringContent(userJsonObject, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("new LinkedList", result); + Assert.Contains("AdditionalData", result); + Assert.Contains("members", result); // property name hasn't been changed + } + + [Fact] + public async Task GeneratesSelectQueryParameters() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/me?$select=displayName,id"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("displayName", result); + Assert.Contains("requestConfiguration ->", result); + Assert.Contains("requestConfiguration.queryParameters.select", 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 GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("\"displayName\"", result); + Assert.Contains("\"id\"", result); + Assert.DoesNotContain("\"true\"", result); + Assert.Contains("true", result); + } + + [Fact] + public async Task GeneratesSkipQueryParameters() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users?$skip=10"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.DoesNotContain("\"10\"", result); + Assert.Contains("10", result); + } + + [Fact] + public async Task GeneratesSelectExpandQueryParameters()/// + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/groups?$expand=members($select=id,displayName)"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("expand", result); + Assert.Contains("members($select=id,displayName)", result); + Assert.DoesNotContain("Select", result); + } + + [Fact] + public async Task GeneratesRequestHeaders() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/groups"); + requestPayload.Headers.Add("ConsistencyLevel", "eventual"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("requestConfiguration.headers.add(\"ConsistencyLevel\", \"eventual\");", result); + Assert.Contains("requestConfiguration ->", result); + } + + [Fact] + public async Task GeneratesFilterParameters() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users?$count=true&$filter=Department eq 'Finance'&$orderBy=displayName&$select=id,displayName,department"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("requestConfiguration.queryParameters.count", result); + Assert.Contains("requestConfiguration.queryParameters.filter", result); + Assert.Contains("requestConfiguration.queryParameters.select", result); + Assert.Contains("requestConfiguration.queryParameters.orderby", result); + } + + [Fact] + public async Task GeneratesFilterParametersWithSpecialCharacters() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users?$filter=imAddresses/any(i:i eq 'admin@contoso.com')"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("requestConfiguration.queryParameters.filter", result); + Assert.Contains("imAddresses/any(i:i eq 'admin@contoso.com')", result); + } + + [Fact] + public async Task GeneratesSnippetForRequestWithDeltaAndSkipToken() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/me/calendarView/delta?$skiptoken=R0usmcCM996atia_s"); + requestPayload.Headers.Add("Prefer", "odata.maxpagesize=2"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("DeltaResponse result = deltaRequestBuilder.get(", result); + Assert.Contains("DeltaRequestBuilder deltaRequestBuilder = new com.microsoft.graph.users.item.calendarview.delta.DeltaRequestBuilder(", result); + } + + [Fact] + public async Task GeneratesSnippetForRequestWithSearchQueryOptionWithANDLogicalConjunction() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users?$search=\"displayName:di\" AND \"displayName:al\""); + requestPayload.Headers.Add("ConsistencyLevel", "eventual"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("requestConfiguration.queryParameters.search", result); + Assert.Contains("requestConfiguration.queryParameters.search = \"\\\"displayName:di\\\" AND \\\"displayName:al\\\"\";", result); + } + + [Fact] + public async Task HandlesOdataTypeWhenGenerating() + { + var sampleJson = @" + { + ""@odata.type"": ""#microsoft.graph.socialIdentityProvider"", + ""displayName"": ""Login with Amazon"", + ""identityProviderType"": ""Amazon"", + ""clientId"": ""56433757-cadd-4135-8431-2c9e3fd68ae8"", + ""clientSecret"": ""000000000000"" + } + "; + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/identity/identityProviders") + { + Content = new StringContent(sampleJson, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("setOdataType(\"#microsoft.graph.socialIdentityProvider\")", result); + Assert.Contains("new SocialIdentityProvider", result);// ensure the derived type is used + } + + [Fact] + public async Task HandlesOdataReferenceSegmentsInUrl() + { + var sampleJson = @" + { + ""@odata.id"": ""https://graph.microsoft.com/beta/users/alexd@contoso.com"" + } + "; + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/groups/id/acceptedSenders/$ref") + { + Content = new StringContent(sampleJson, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains(".acceptedSenders().ref().post(", result); + } + + [Fact] + public async Task GenerateSnippetsWithArrayNesting() + { + var eventData = @" + { + ""subject"": ""Let's go for lunch"", + ""body"": { + ""contentType"": ""Html"", + ""content"": ""Does noon work for you?"" + }, + ""start"": { + ""dateTime"": ""2017-04-15T12:00:00"", + ""timeZone"": ""Pacific Standard Time"" + }, + ""end"": { + ""dateTime"": ""2017-04-15T14:00:00"", + ""timeZone"": ""Pacific Standard Time"" + }, + ""location"":{ + ""displayName"": null + }, + ""attendees"": [ + { + ""emailAddress"": { + ""address"":""samanthab@contoso.onmicrosoft.com"", + ""name"": ""Samantha Booth"" + }, + ""type"": ""required"" + }, + { + ""emailAddress"": { + ""address"":""ss@contoso.com"", ""name"": ""Sorry Sir""}, ""type"":""Optional"" + } + ], + ""allowNewTimeProposals"": true, + ""transactionId"":""7E163156-7762-4BEB-A1C6-729EA81755A7"" + }"; + + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/events") + { + Content = new StringContent(eventData, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("setContentType(BodyType.Html)", result); + Assert.Contains("new LinkedList()", result); + Assert.Contains("setAttendees(attendees)", result); + Assert.Contains("setStart(start)", result); + Assert.Contains("setDisplayName(null)", result); + } + + [Fact] + public async Task GenerateFindMeetingTime() + { + var bodyContent = @" + { + ""attendees"": [ + { + ""emailAddress"": { + ""address"": ""{user-mail}"", + ""name"": ""Alex Darrow"" + }, + ""type"": ""Required"" + } + ], + ""timeConstraint"": { + ""timeSlots"": [ + { + ""start"": { + ""dateTime"": ""2022-07-18T13:24:57.384Z"", + ""timeZone"": ""Pacific Standard Time"" + }, + ""end"": { + ""dateTime"": ""2022-07-25T13:24:57.384Z"", + ""timeZone"": ""Pacific Standard Time"" + } + } + ] + }, + ""locationConstraint"": { + ""isRequired"": ""false"", + ""suggestLocation"": ""true"", + ""locations"": [ + { + ""displayName"": ""Conf Room 32/1368"", + ""locationEmailAddress"": ""conf32room1368@imgeek.onmicrosoft.com"" + } + ] + }, + ""meetingDuration"": ""PT1H"" + }"; + + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/findMeetingTimes") + { + Content = new StringContent(bodyContent, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("meetingDuration = PeriodAndDuration.ofDuration(Duration.parse(\"PT1H\"));", result); + Assert.Contains("setMeetingDuration(meetingDuration)", result); + Assert.Contains("setIsRequired(false)", result); + Assert.Contains("setLocationEmailAddress(\"conf32room1368@imgeek.onmicrosoft.com\")", result); + } + + [Theory] + [InlineData("sendMail")] + [InlineData("microsoft.graph.sendMail")] + public async Task FullyQualifiesActionRequestBodyType(string sendMailString) + { + var bodyContent = @"{ + ""message"": { + ""subject"": ""Meet for lunch?"", + ""body"": { + ""contentType"": ""Text"", + ""content"": ""The new cafeteria is open."" + }, + ""toRecipients"": [ + { + ""emailAddress"": { + ""address"": ""fannyd@contoso.onmicrosoft.com"" + } + } + ], + ""ccRecipients"": [ + { + ""emailAddress"": { + ""address"": ""danas@contoso.onmicrosoft.com"" + } + } + ] + }, + ""saveToSentItems"": ""false"" + }"; + + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/users/{{id}}/{sendMailString}") + { + Content = new StringContent(bodyContent, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("graphClient.users().byUserId(\"{user-id}\").sendMail().post(sendMailPostRequestBody).get();", result); + Assert.Contains("SendMailPostRequestBody sendMailPostRequestBody = new com.microsoft.graph.users.item.sendmail.SendMailPostRequestBody()", result); + Assert.Contains("toRecipients = new LinkedList", result); + } + + [Fact] + public async Task TypeArgumentsForListArePlacedCorrectly() + { + var bodyContent = @"{ + ""businessPhones"": [ + ""+1 425 555 0109"" + ], + ""officeLocation"": ""18/2111"" + }"; + + using var requestPayload = new HttpRequestMessage(HttpMethod.Patch, $"{ServiceRootUrl}/users/{{id}}") + { + Content = new StringContent(bodyContent, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("new LinkedList", result); + } + + [Fact] + public async Task ModelsInNestedNamespacesAreDisambiguated() + { + var bodyContent = @"{ + ""id"": ""1431b9c38ee647f6a"", + ""type"": ""externalGroup"", + }"; + + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/external/connections/contosohr/groups/31bea3d537902000/members") + { + Content = new StringContent(bodyContent, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("new com.microsoft.graph.models.externalconnectors.Identity", result); + Assert.Contains("setType(com.microsoft.graph.models.externalconnectors.IdentityType.ExternalGroup)", result); + } + + [Fact] + public async Task ReplacesReservedTypeNames() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/directory/administrativeUnits/8a07f5a8-edc9-4847-bbf2-dde106594bf4/scopedRoleMembers"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + // Assert `Directory` is replaced with `DirectoryObject` + Assert.Contains("graphClient.directory().administrativeUnits().byAdministrativeUnitId(\"{administrativeUnit-id}\").scopedRoleMembers().get()", result); + } + + [Fact] + public async Task CorrectlyGeneratesEnumMember() + { + var bodyContent = @"{ + ""id"": ""SHPR_eeab4fb1-20e5-48ca-ad9b-98119d94bee7"", + ""@odata.etag"": ""1a371e53-f0a6-4327-a1ee-e3c56e4b38aa"", + ""availability"": [ + { + ""recurrence"": { + ""pattern"": { + ""type"": ""Weekly"", + ""interval"": 1 + }, + ""range"": { + ""type"": ""noEnd"" + } + }, + ""timeZone"": ""Pacific Standard Time"", + ""timeSlots"": null + } + ] + }"; + + using var requestPayload = new HttpRequestMessage(HttpMethod.Patch, $"{ServiceRootUrl}/users/871dbd5c-3a6a-4392-bfe1-042452793a50/settings/shiftPreferences") + { + Content = new StringContent(bodyContent, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("setType(RecurrencePatternType.Weekly)", result); + Assert.Contains("setType(RecurrenceRangeType.NoEnd)", result); + } + + [Fact] + public async Task CorrectlyGeneratesMultipleFlagsEnumMembers() + { + var bodyContent = @"{ + ""clientContext"": ""clientContext-value"", + ""status"": ""notRecorDing | recording , failed""}"; //Ensure enums are split by comma or pipe + + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/communications/calls/{{id}}/updateRecordingStatus") + { + Content = new StringContent(bodyContent, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("setStatus(RecordingStatus.NotRecording | RecordingStatus.Recording | RecordingStatus.Failed)", result); + } + + [Fact] + public async Task CorrectlyOptionalRequestBodyParameter() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/teams/{{id}}/archive"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("graphClient.teams().byTeamId(\"{team-id}\").archive().post(null).get()", result); + } + + [Fact] + public async Task CorrectlyEvaluatesDatePropertyTypeRequestBodyParameter() + { + var bodyContent = @"{ + ""subject"": ""Let's go for lunch"", + ""recurrence"": { + ""range"": { + ""type"": ""endDate"", + ""startDate"": ""2017-09-04"", + ""endDate"": ""2017-12-31"" + } + } + }"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/events") + { + Content = new StringContent(bodyContent, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("startDate = LocalDate.parse(\"2017-09-04\");", result); + Assert.Contains("endDate = LocalDate.parse(\"2017-12-31\");", result); + Assert.Contains("setStartDate(startDate)", result); + Assert.Contains("setEndDate(endDate)", result); + } + + [Fact] + public async Task CorrectlyEvaluatesOdataActionRequestBodyParameter() + { + var bodyContent = @"{ + ""keyCredential"": { + ""type"": ""AsymmetricX509Cert"", + ""usage"": ""Verify"", + ""key"": ""MIIDYDCCAki..."" + }, + ""passwordCredential"": null, + ""proof"":""eyJ0eXAiOiJ..."" + }"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/applications/{{id}}/addKey") + { + Content = new StringContent(bodyContent, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("AddKeyPostRequestBody addKeyPostRequestBody = new com.microsoft.graph.applications.item.addkey.AddKeyPostRequestBody()", result); + } + + [Fact] + public async Task CorrectlyEvaluatesGuidInRequestBodyParameter() + { + var bodyContent = @"{ + ""principalId"": ""cde330e5-2150-4c11-9c5b-14bfdc948c79"", + ""resourceId"": ""8e881353-1735-45af-af21-ee1344582a4d"", + ""appRoleId"": ""00000000-0000-0000-0000-000000000000"" + }"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/users/{{id}}/appRoleAssignments") + { + Content = new StringContent(bodyContent, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("UUID.fromString(\"cde330e5-2150-4c11-9c5b-14bfdc948c79\")", result); + Assert.Contains("UUID.fromString(\"8e881353-1735-45af-af21-ee1344582a4d\")", result); + Assert.Contains("UUID.fromString(\"00000000-0000-0000-0000-000000000000\")", result); + } + + [Fact] + public async Task DefaultsEnumIfNoneProvided() + { + var bodyContent = @"{ + ""subject"": ""subject-value"", + ""body"": { + ""contentType"": """", + ""content"": ""content-value"" + }, + ""inferenceClassification"": ""other"" + }"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Patch, $"{ServiceRootUrl}/me/messages/{{id}}") + { + Content = new StringContent(bodyContent, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("setContentType(BodyType.Text)", result); + } + + [Fact] + public async Task HandlesEmptyCollection() + { + var bodyContent = @"{ + ""defaultUserRolePermissions"": { + ""permissionGrantPoliciesAssigned"": [] + } + }"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Patch, $"{ServiceRootUrl}/policies/authorizationPolicy") + { + Content = new StringContent(bodyContent, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("permissionGrantPoliciesAssigned = new LinkedList", result); + } + + [Fact] + public async Task CorrectlyHandlesOdataFunction() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/users/delta?$select=displayName,jobTitle,mobilePhone"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("graphClient.users().delta().get(", result); + Assert.Contains("requestConfiguration.queryParameters.select = new String []{\"displayName\", \"jobTitle\", \"mobilePhone\"};", result); + } + + [Fact] + public async Task CorrectlyHandlesDateTimeOffsetInUrl() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/reports/getUserArchivedPrintJobs(userId='{{id}}',startDateTime=,endDateTime=)"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("graphClient.reports().getUserArchivedPrintJobsWithUserIdWithStartDateTimeWithEndDateTime(OffsetDateTime.parse(\"{endDateTime}\"), OffsetDateTime.parse(\"{startDateTime}\"), \"{userId}\").get().get()", result); + } + + [Fact] + public async Task CorrectlyHandlesNumberInUrl() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/me/drive/items/{{id}}/workbook/worksheets/{{id|name}}/cell(row=,column=)"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("graphClient.drives().byDriveId(\"{drive-id}\").items().byDriveItemId(\"{driveItem-id}\").workbook().worksheets().byWorkbookWorksheetId(\"{workbookWorksheet-id}\").cellWithRowWithColumn(1, 1).get()", result); + } + + [Fact] + public async Task CorrectlyHandlesDateInUrl() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/reports/getYammerGroupsActivityDetail(date='2018-03-05')"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("graphClient.reports().getYammerGroupsActivityDetailWithDate(LocalDate.parse(\"{date}\")).get()", result); + } + + [Fact] + public async Task CorrectlyHandlesDateInUrl2() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/communications/callRecords/getPstnCalls(fromDateTime=2019-11-01,toDateTime=2019-12-01)"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("graphClient.communications().callRecords().microsoftGraphCallRecordsGetPstnCallsWithFromDateTimeWithToDateTime(OffsetDateTime.parse(\"{fromDateTime}\"), OffsetDateTime.parse(\"{toDateTime}\")).get()", result); + } + + [Fact] + public async Task CorrectlyHandlesEnumInUrl() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/identityGovernance/appConsent/appConsentRequests/filterByCurrentUser(on='reviewer')?$filter=userConsentRequests/any(u:u/status eq 'InProgress')"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("graphClient.identityGovernance().appConsent().appConsentRequests().filterByCurrentUserWithOn(\"reviewer\").get", result); + } + + [Fact] + public async Task GeneratesObjectsInArray() + { + var sampleJson = @" + { + ""addLicenses"": [ + { + ""disabledPlans"": [ ""11b0131d-43c8-4bbb-b2c8-e80f9a50834a"" ], + ""skuId"": ""45715bb8-13f9-4bf6-927f-ef96c102d394"" + } + ], + ""removeLicenses"": [ ""bea13e0c-3828-4daa-a392-28af7ff61a0f"" ] + } + "; + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/assignLicense") + { + Content = new StringContent(sampleJson, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("com.microsoft.graph.users.item.assignlicense.AssignLicensePostRequestBody assignLicensePostRequestBody = new com.microsoft.graph.users.item.assignlicense.AssignLicensePostRequestBody();", result); + Assert.Contains("LinkedList disabledPlans = new LinkedList", result); + Assert.Contains("LinkedList removeLicenses = new LinkedList", result); + Assert.Contains("UUID.fromString(\"bea13e0c-3828-4daa-a392-28af7ff61a0f\")", result); + } + + [Fact] + public async Task GeneratesCorrectCollectionTypeAndDerivedInstances() + { + var sampleJson = @"{ + ""message"": { + ""subject"": ""Meet for lunch?"", + ""body"": { + ""contentType"": ""Text"", + ""content"": ""The new cafeteria is open."" + }, + ""toRecipients"": [ + { + ""emailAddress"": { + ""address"": ""meganb@contoso.onmicrosoft.com"" + } + } + ], + ""attachments"": [ + { + ""@odata.type"": ""#microsoft.graph.fileAttachment"", + ""name"": ""attachment.txt"", + ""contentType"": ""text/plain"", + ""contentBytes"": ""SGVsbG8gV29ybGQh"" + } + ] + } + }"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Post, $"{ServiceRootUrl}/me/sendMail") + { + Content = new StringContent(sampleJson, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("SendMailPostRequestBody sendMailPostRequestBody = new com.microsoft.graph.users.item.sendmail.SendMailPostRequestBody()", result); + Assert.Contains("LinkedList attachments = new LinkedList", result);// Collection defines Base type + Assert.Contains("new FileAttachment", result);// Individual items are derived types + Assert.Contains("byte[] contentBytes = Base64.getDecoder().decode(\"SGVsbG8gV29ybGQh\")", result); + } + + [Fact] + public async Task GeneratesPropertiesWithSpecialCharacters() + { + var sampleJson = @"{ + ""@odata.type"": ""#microsoft.graph.managedIOSLobApp"", + ""displayName"": ""Display Name value"", + ""description"": ""Description value"", + ""publisher"": ""Publisher value"", + ""largeIcon"": { + ""@odata.type"": ""microsoft.graph.mimeContent"", + ""type"": ""Type value"", + ""value"": ""dmFsdWU="" + }, + ""isFeatured"": true, + ""privacyInformationUrl"": ""https://example.com/privacyInformationUrl/"", + ""informationUrl"": ""https://example.com/informationUrl/"", + ""owner"": ""Owner value"", + ""developer"": ""Developer value"", + ""notes"": ""Notes value"", + ""uploadState"": 11, + ""publishingState"": ""processing"", + ""isAssigned"": true, + ""roleScopeTagIds"": [ + ""Role Scope Tag Ids value"" + ], + ""dependentAppCount"": 1, + ""supersedingAppCount"": 3, + ""supersededAppCount"": 2, + ""appAvailability"": ""lineOfBusiness"", + ""version"": ""Version value"", + ""committedContentVersion"": ""Committed Content Version value"", + ""fileName"": ""File Name value"", + ""size"": 4, + ""bundleId"": ""Bundle Id value"", + ""applicableDeviceType"": { + ""@odata.type"": ""microsoft.graph.iosDeviceType"", + ""iPad"": true, + ""iPhoneAndIPod"": true + }, + ""minimumSupportedOperatingSystem"": { + ""@odata.type"": ""microsoft.graph.iosMinimumOperatingSystem"", + ""v8_0"": true, + ""v9_0"": true, + ""v10_0"": true, + ""v11_0"": true, + ""v12_0"": true, + ""v13_0"": true, + ""v14_0"": true, + ""v15_0"": true, + ""v16_0"": true + }, + ""expirationDateTime"": ""2016-12-31T23:57:57.2481234-08:00"", + ""versionNumber"": ""Version Number value"", + ""buildNumber"": ""Build Number value"", + ""identityVersion"": ""Identity Version value"" + }"; + using var requestPayload = new HttpRequestMessage(HttpMethod.Patch, $"{ServiceRootBetaUrl}/deviceAppManagement/mobileApps/{{mobileAppId}}") + { + Content = new StringContent(sampleJson, Encoding.UTF8, "application/json") + }; + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains("IosMinimumOperatingSystem minimumSupportedOperatingSystem = new IosMinimumOperatingSystem", result); + Assert.Contains("setV80(true)", result); //assert property name is pascal case after the 'set' portion + } + + [Fact] + public async Task CorrectlyHandlesTypeFromInUrl() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/me/mailFolders/?includehiddenfolders=true"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("requestConfiguration.queryParameters.includeHiddenFolders = \"true\";", result); + } + + [Fact] + public async Task MatchesPathWithPathParameter() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/me/drive/items/{{id}}/workbook/worksheets/{{id|name}}/range(address='A1:B2')"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("var result = graphClient.drives().byDriveId(\"{drive-id}\").items().byDriveItemId(\"{driveItem-id}\").workbook().worksheets().byWorkbookWorksheetId(\"{workbookWorksheet-id}\").rangeWithAddress(\"{address}\").get()", result); + } + + [Fact] + public async Task MatchesPathAlternateKeys() + { + using var requestPayload = new HttpRequestMessage(HttpMethod.Get, $"{ServiceRootUrl}/applications(appId='46e6adf4-a9cf-4b60-9390-0ba6fb00bf6b')?$select=id,appId,displayName,requiredResourceAccess"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + + Assert.Contains("graphClient.applicationsWithAppId(\"{appId}\").get(", result); + } + + [Theory] + [InlineData("/me/drive/root/delta", "graphClient.drives().byDriveId(\"{drive-id}\").items().byDriveItemId(\"{driveItem-id}\").delta().get()")] + [InlineData("/groups/{group-id}/drive/items/{item-id}/children", "graphClient.drives().byDriveId(\"{drive-id}\").items().byDriveItemId(\"{driveItem-id}\").children().get()")] + [InlineData("/me/drive", "graphClient.me().drive().get()")] + [InlineData("/sites/{site-id}/drive/items/{item-id}/children", "graphClient.drives().byDriveId(\"{drive-id}\").items().byDriveItemId(\"{driveItem-id}\").children().get()")] + [InlineData("/sites/{site-id}/drive/root/children", "graphClient.drives().byDriveId(\"{drive-id}\").items().byDriveItemId(\"{driveItem-id}\").children().get()")] + [InlineData("/users/{user-id}/drive/items/{item-id}/children", "graphClient.drives().byDriveId(\"{drive-id}\").items().byDriveItemId(\"{driveItem-id}\").children().get()")] + [InlineData("/me/drive/items/{item-id}/children", "graphClient.drives().byDriveId(\"{drive-id}\").items().byDriveItemId(\"{driveItem-id}\").children().get()")] + [InlineData("/drive/bundles", "graphClient.drives().byDriveId(\"{drive-id}\").bundles().get()")] + [InlineData("/me/drive/special/documents", "graphClient.drives().byDriveId(\"{drive-id}\").special().byDriveItemId(\"{driveItem-id}\").get()")] + [InlineData("/me/drive/root/search(q='Contoso%20Project')", "graphClient.drives().byDriveId(\"{drive-id}\").items().byDriveItemId(\"{driveItem-id}\").searchWithQ(\"{q}\").get()")] + [InlineData("/me/drive/items/{id}/workbook/application/calculate", "graphClient.drives().byDriveId(\"{drive-id}\").items().byDriveItemId(\"{driveItem-id}\").workbook().application().calculate()", "POST")] + public async Task GeneratesSnippetWithRemappedDriveCall(string inputPath, string expected, string method = "") + { + using var requestPayload = new HttpRequestMessage(string.IsNullOrEmpty(method) ? HttpMethod.Get : new HttpMethod(method), $"{ServiceRootUrl}{inputPath}"); + var snippetModel = new SnippetModel(requestPayload, ServiceRootUrl, await GetV1SnippetMetadata()); + var result = _generator.GenerateCodeSnippet(snippetModel); + Assert.Contains(expected, result); + } + } +} diff --git a/CodeSnippetsReflection.OpenAPI/LanguageGenerators/JavaGenerator.cs b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/JavaGenerator.cs new file mode 100644 index 000000000..59cb8e4c6 --- /dev/null +++ b/CodeSnippetsReflection.OpenAPI/LanguageGenerators/JavaGenerator.cs @@ -0,0 +1,468 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using CodeSnippetsReflection.OpenAPI.ModelGraph; +using CodeSnippetsReflection.StringExtensions; +using Microsoft.OpenApi.Services; + +namespace CodeSnippetsReflection.OpenAPI.LanguageGenerators +{ + public class JavaGenerator : ILanguageGenerator + { + // Constants for the code snippet + private const string ClientVarName = "graphClient"; + private const string ClientVarType = "GraphServiceClient"; + private const string HttpCoreVarName = "requestAdapter"; + private const string RequestConfigurationVarName = "requestConfiguration"; + private const string RequestHeadersPropertyName = "headers"; + private const string RequestParametersPropertyName = "queryParameters"; + + // Constants for reading from the OpenAPI document + private const string ModelNamespacePrefixToTrim = $"models.{DefaultNamespace}"; + private const string DefaultNamespace = "microsoft.graph"; + + private static readonly HashSet ReservedNames = new(StringComparer.OrdinalIgnoreCase) + { + "abstract", + "assert", + "boolean", + "break", + "byte", + "case", + "catch", + "char", + "class", + "const", + "continue", + "default", + "double", + "do", + "else", + "enum", + "extends", + "false", + "final", + "finally", + "float", + "for", + "goto", + "if", + "implements", + "import", + "instanceof", + "int", + "interface", + "long", + "native", + "new", + "null", + "package", + "private", + "protected", + "public", + "return", + "short", + "static", + "strictfp", + "super", + "switch", + "synchronized", + "this", + "throw", + "throws", + "transient", + "true", + "try", + "void", + "volatile", + "while" + }; + + public string GenerateCodeSnippet(SnippetModel snippetModel) + { + var indentManager = new IndentManager(); + var codeGraph = new SnippetCodeGraph(snippetModel); + var snippetBuilder = new StringBuilder($"// Code snippets are only available for the latest version. Current version is 6.x{Environment.NewLine}{Environment.NewLine}" + + $"{ClientVarType} {ClientVarName} = new {ClientVarType}({HttpCoreVarName});{Environment.NewLine}{Environment.NewLine}"); + + WriteRequestPayloadAndVariableName(codeGraph, snippetBuilder); + WriteRequestExecutionPath(codeGraph, snippetBuilder, indentManager); + return snippetBuilder.ToString(); + } + + private static void WriteRequestExecutionPath(SnippetCodeGraph codeGraph, StringBuilder payloadSb, IndentManager indentManager) + { + var responseAssignment = codeGraph.HasReturnedBody() ? $"{codeGraph.ResponseSchema.Reference?.Id.Replace($"{DefaultNamespace}.", string.Empty).ToPascalCase() ?? "var"} result = " : string.Empty; + var methodName = codeGraph.HttpMethod.Method.ToLower(); + + string requestPayloadParameterName = default; + if (codeGraph.HasBody()) + { + requestPayloadParameterName = codeGraph.Body.PropertyType switch + { + PropertyType.Binary => "stream", + _ => GetPropertyObjectName(codeGraph.Body).ToFirstCharacterLowerCase() + }; + } + if (string.IsNullOrEmpty(requestPayloadParameterName) && ((codeGraph.RequestSchema?.Properties?.Any() ?? false) || (codeGraph.RequestSchema?.AllOf?.Any(schema => schema.Properties.Any()) ?? false))) + requestPayloadParameterName = "null";// pass a null parameter if we have a request schema expected but there is not body provided + + string pathSegment; + if(codeGraph.Nodes.Last().Segment.Contains("delta") && + codeGraph.Parameters.Any( static property => property.Name.Equals("skiptoken",StringComparison.OrdinalIgnoreCase) || + property.Name.Equals("deltatoken",StringComparison.OrdinalIgnoreCase))) + {// its a delta query and needs the opaque url passed over. + var deltaNamespace = $"com.{GetDefaultNamespaceName(codeGraph.ApiVersion)}.{GetFluentApiPath(codeGraph.Nodes, codeGraph, true).Replace("()", "").Replace("me.", "users.item.").ToLowerInvariant()}"; + pathSegment = "deltaRequestBuilder."; + codeGraph.Parameters = new List();// clear the query parameters as these will be provided in the url directly. + payloadSb.AppendLine($"{deltaNamespace}DeltaRequestBuilder deltaRequestBuilder = new {deltaNamespace}DeltaRequestBuilder(\"{codeGraph.RequestUrl}\", {ClientVarName}.getRequestAdapter());"); + responseAssignment = $"{deltaNamespace}DeltaResponse result = "; + } + else + { + pathSegment = $"{ClientVarName}.{GetFluentApiPath(codeGraph.Nodes, codeGraph)}"; + } + + var requestConfigurationPayload = GetRequestConfiguration(codeGraph, indentManager); + var parametersList = GetActionParametersList(requestPayloadParameterName , requestConfigurationPayload); + payloadSb.AppendLine($"{responseAssignment}{pathSegment}{methodName}({parametersList}).get();"); + } + + private static string GetRequestConfiguration(SnippetCodeGraph snippetCodeGraph, IndentManager indentManager) + { + if (!snippetCodeGraph.HasHeaders() && !snippetCodeGraph.HasParameters()) + return default; + + var requestConfigurationBuilder = new StringBuilder(); + requestConfigurationBuilder.AppendLine($"{RequestConfigurationVarName} -> {{"); + WriteRequestQueryParameters(snippetCodeGraph, indentManager, requestConfigurationBuilder); + WriteRequestHeaders(snippetCodeGraph, indentManager, requestConfigurationBuilder); + requestConfigurationBuilder.Append('}'); + return requestConfigurationBuilder.ToString(); + } + + private static void WriteRequestQueryParameters(SnippetCodeGraph snippetCodeGraph, IndentManager indentManager, StringBuilder stringBuilder) + { + if (!snippetCodeGraph.HasParameters()) + return; + + indentManager.Indent(); + foreach (var queryParam in snippetCodeGraph.Parameters) + { + stringBuilder.AppendLine($"{indentManager.GetIndent()}{RequestConfigurationVarName}.{RequestParametersPropertyName}.{queryParam.Name.ToFirstCharacterLowerCase()} = {GetQueryParameterValue(queryParam)};"); + } + indentManager.Unindent(); + } + + private static string GetActionParametersList(params string[] parameters) + { + var nonEmptyParameters = parameters.Where(static p => !string.IsNullOrEmpty(p)); + return nonEmptyParameters.Any() ? string.Join(", ", nonEmptyParameters.Aggregate(static (a, b) => $"{a}, {b}")) : string.Empty; + } + + private static void WriteRequestHeaders(SnippetCodeGraph snippetCodeGraph, IndentManager indentManager, StringBuilder stringBuilder) + { + if (!snippetCodeGraph.HasHeaders()) + return; + + indentManager.Indent(); + foreach (var header in snippetCodeGraph.Headers) + { + stringBuilder.AppendLine($"{indentManager.GetIndent()}{RequestConfigurationVarName}.{RequestHeadersPropertyName}.add(\"{header.Name}\", \"{header.Value.EscapeQuotes()}\");"); + } + indentManager.Unindent(); + } + + private static string GetQueryParameterValue(CodeProperty queryParam) + { + switch (queryParam.PropertyType) + { + case PropertyType.Boolean: + return queryParam.Value.ToLowerInvariant(); // Boolean types + case PropertyType.Int32: + case PropertyType.Int64: + case PropertyType.Double: + case PropertyType.Float32: + case PropertyType.Float64: + return queryParam.Value; // Numbers stay as is + case PropertyType.Array: + return $"new String []{{{string.Join(", ", queryParam.Children.Select(static x => $"\"{x.Value}\"").ToList())}}}"; // deconstruct arrays + default: + return $"\"{queryParam.Value.EscapeQuotes()}\""; + } + } + + private static string GetFluentApiPath(IEnumerable nodes, SnippetCodeGraph codeGraph, bool isDeltaNamespce = false) + { + if (!(nodes?.Any() ?? false)) return string.Empty; + var elements = nodes.Select(x => + { + if (x.Segment.IsCollectionIndex()) + { + if (isDeltaNamespce) + return "item."; + + var pathName = string.IsNullOrEmpty(x.Segment) ? x.Segment : x.Segment.ReplaceMultiple("", "{", "}") + .Split('-') + .Where(static s => !string.IsNullOrEmpty(s)) + .Select(static s => s.ToFirstCharacterUpperCase()) + .Aggregate(static (a, b) => $"By{a}{b}"); + return $"{pathName ?? "ByTypeId"}{x.Segment.Replace("{", "(\"{", StringComparison.OrdinalIgnoreCase).Replace("}", "}\")", StringComparison.OrdinalIgnoreCase)}."; + } + if (x.Segment.IsFunctionWithParameters()) + { + var functionName = x.Segment.Split('(')[0] + .Split(".", StringSplitOptions.RemoveEmptyEntries) + .Select(static s => s.ToPascalCase()) + .Aggregate(static (a, b) => $"{a}{b}"); + + var parameters = codeGraph.PathParameters + .Select(static s => $"With{s.Name.ToPascalCase()}") + .Aggregate(static (a, b) => $"{a}{b}"); + + string parameterDeclarations = string.Join(", ", codeGraph.PathParameters + .OrderBy(static parameter => parameter.Name, StringComparer.OrdinalIgnoreCase) + .Select(parameter => GetPathParameterDeclaration(parameter, codeGraph.ApiVersion))); + return $"{functionName.ToFirstCharacterLowerCase()}{parameters}({parameterDeclarations})."; + } + if (x.Segment.IsFunction()) + { + return x.Segment.Split('.') + .Select(static s => s.ToFirstCharacterLowerCase()) + .Aggregate(static (a, b) => $"{a}{b}").ToFirstCharacterUpperCase()+"()."; + } + return x.Segment.ToFirstCharacterLowerCase()+"()."; + }) + .Aggregate(new List(), static (current, next) => + { + var element = next.Contains("ByTypeId", StringComparison.OrdinalIgnoreCase) ? + next.Replace("ByTypeId", $"By{current[current.Count-1].Replace("s().", string.Empty, StringComparison.OrdinalIgnoreCase)}Id") : + $"{next.Replace("$", string.Empty, StringComparison.OrdinalIgnoreCase).ToFirstCharacterLowerCase()}"; + + current.Add(element); + return current; + }); + + return string.Join("", elements).Replace("()()", "()").Replace("..","."); + } + + private static string GetPathParameterDeclaration(CodeProperty pathParameter, string apiVersion) + { + switch (pathParameter.PropertyType) + { + case PropertyType.String: + return $"\"{pathParameter.Value}\""; + case PropertyType.Null: + case PropertyType.Boolean: + return pathParameter.Value.ToLowerInvariant(); + case PropertyType.Enum: + return pathParameter.Value.ToFirstCharacterUpperCase(); + case PropertyType.DateTime: + case PropertyType.DateOnly: + case PropertyType.TimeOnly: + return $"{GetTypeString(pathParameter, apiVersion)}.parse(\"{pathParameter.Value}\")"; + case PropertyType.Duration: + return $"PeriodAndDuration.ofDuration(Duration.parse(\"{pathParameter.Value}\"))"; + case PropertyType.Float32: + case PropertyType.Float64: + return $"{pathParameter.Value}f"; + case PropertyType.Int64: + return $"{pathParameter.Value}L"; + case PropertyType.Double: + return $"{pathParameter.Value}d"; + default: + return pathParameter.Value; + } + } + + private static string ReplaceIfReservedName(string originalString, string suffix = "Object") + => ReservedNames.Contains(originalString) ? $"{originalString}{suffix}" : originalString; + + private static void WriteRequestPayloadAndVariableName(SnippetCodeGraph snippetCodeGraph, StringBuilder snippetBuilder) + { + if (!snippetCodeGraph.HasBody()) + return;// No body + switch (snippetCodeGraph.Body.PropertyType) + { + case PropertyType.Object: + var typeString = GetTypeString(snippetCodeGraph.Body, snippetCodeGraph.ApiVersion); + var objectName = GetPropertyObjectName(snippetCodeGraph.Body).ToFirstCharacterLowerCase(); + List usedVariableNames = new List() {objectName}; + snippetBuilder.AppendLine($"{typeString} {objectName} = new {typeString}();"); + snippetCodeGraph.Body.Children.ForEach( child => WriteObjectFromCodeProperty(snippetCodeGraph.Body, child, snippetBuilder,snippetCodeGraph.ApiVersion, objectName, usedVariableNames)); + break; + case PropertyType.Binary: + snippetBuilder.AppendLine($"ByteArrayInputStream stream = new ByteArrayInputStream(new byte[0]); //stream to upload"); + break; + default: + throw new InvalidOperationException($"Unsupported property type for request: {snippetCodeGraph.Body.PropertyType}"); + } + } + + private static void WriteObjectFromCodeProperty(CodeProperty parentProperty, CodeProperty codeProperty, StringBuilder snippetBuilder, string apiVersion, string parentPropertyName, List usedVariableNames) + { + var setterPrefix = "set"; + var typeString = GetTypeString(codeProperty, apiVersion); + var currentPropertyName = EnsureJavaVariableNameIsUnique($"{GetPropertyObjectName(codeProperty)?.ToFirstCharacterLowerCase() ?? "property"}", usedVariableNames); + + var isParentArray = parentProperty.PropertyType == PropertyType.Array; + var isParentMap = parentProperty.PropertyType == PropertyType.Map; + + var propertyAssignment = $"{parentPropertyName}.{setterPrefix}{GetPropertyObjectName(codeProperty)}("; // default assignments to the usual "primitive x = setX();" + propertyAssignment = isParentArray ? $"{parentPropertyName}.add(" : propertyAssignment; + propertyAssignment = isParentMap ? $"{parentPropertyName}.put(\"{codeProperty.Name}\", " : propertyAssignment; + + var assignment = $"{propertyAssignment}{currentPropertyName ?? "property"});"; + + switch (codeProperty.PropertyType) + { + case PropertyType.Object: + snippetBuilder.AppendLine($"{typeString} {currentPropertyName} = new {typeString}();"); + codeProperty.Children.ForEach(child => WriteObjectFromCodeProperty(codeProperty, child, snippetBuilder, apiVersion, currentPropertyName, usedVariableNames)); + snippetBuilder.AppendLine(assignment); + break; + case PropertyType.Array: + case PropertyType.Map: + snippetBuilder.AppendLine($"{typeString} {currentPropertyName} = new {typeString}();"); + codeProperty.Children.ForEach(child => { + WriteObjectFromCodeProperty(codeProperty, child, snippetBuilder, apiVersion, currentPropertyName, usedVariableNames); + }); + snippetBuilder.AppendLine(assignment); + break; + case PropertyType.Guid: + snippetBuilder.AppendLine($"{propertyAssignment}UUID.fromString(\"{codeProperty.Value}\"));"); + break; + case PropertyType.Enum: + var enumTypeString = GetTypeString(codeProperty, apiVersion); + var enumValues = codeProperty.Value.Split(new []{'|',','}, StringSplitOptions.RemoveEmptyEntries) + .Select(x => + { + var enumHint = x.Split('.').Last().Trim(); + // the enum member may be invalid so default to generating the first value in case a look up fails. + var enumMember = codeProperty.Children.Find(member => member.Value.Equals(enumHint, StringComparison.OrdinalIgnoreCase)).Value ?? codeProperty.Children.FirstOrDefault().Value ?? enumHint; + return $"{enumTypeString}.{enumMember.ToFirstCharacterUpperCase()}"; + }) + .Aggregate(static (x, y) => $"{x} | {y}"); + snippetBuilder.AppendLine($"{propertyAssignment}{enumValues});"); + break; + case PropertyType.DateTime: + case PropertyType.DateOnly: + case PropertyType.TimeOnly: + snippetBuilder.AppendLine($"{typeString} {currentPropertyName} = {typeString}.parse(\"{codeProperty.Value}\");"); + snippetBuilder.AppendLine(assignment); + break; + case PropertyType.Base64Url: + snippetBuilder.AppendLine($"byte[] {currentPropertyName} = Base64.getDecoder().decode(\"{codeProperty.Value.EscapeQuotes()}\");"); + snippetBuilder.AppendLine(assignment); + break; + case PropertyType.Float32: + case PropertyType.Float64: + snippetBuilder.AppendLine($"{propertyAssignment}{codeProperty.Value}f);"); + break; + case PropertyType.Int64: + snippetBuilder.AppendLine($"{propertyAssignment}{codeProperty.Value}L);"); + break; + case PropertyType.Double: + snippetBuilder.AppendLine($"{propertyAssignment}{codeProperty.Value}d);"); + break; + case PropertyType.Null: + case PropertyType.Int32: + case PropertyType.Boolean: + snippetBuilder.AppendLine($"{propertyAssignment}{codeProperty.Value.ToFirstCharacterLowerCase()});"); + break; + case PropertyType.Duration: + snippetBuilder.AppendLine($"{typeString} {currentPropertyName} = PeriodAndDuration.ofDuration(Duration.parse(\"{codeProperty.Value}\"));"); + snippetBuilder.AppendLine(assignment); + break; + case PropertyType.Binary: + case PropertyType.Default: + case PropertyType.String: + snippetBuilder.AppendLine($"{propertyAssignment}\"{codeProperty.Value}\");"); + break; + default: + snippetBuilder.AppendLine($"{propertyAssignment}\"{codeProperty.Value}\");"); + break; + } + } + + private const string StringTypeName = "String"; + + private static string GetTypeString(CodeProperty codeProperty, string apiVersion) + { + var typeString = codeProperty.TypeDefinition.ToFirstCharacterUpperCase() ?? + codeProperty.Value.ToFirstCharacterUpperCase(); + switch (codeProperty.PropertyType) + { + case PropertyType.Array: + // For objects, rely on the typeDefinition from the array definition otherwise look deeper for primitive collections + var collectionTypeString = codeProperty.Children.Any() && codeProperty.Children[0].PropertyType != PropertyType.Object + ? GetTypeString(codeProperty.Children[0], apiVersion) + : ReplaceIfReservedName(typeString); + if(string.IsNullOrEmpty(collectionTypeString)) + collectionTypeString = "Object"; + else if(typeString.Equals(StringTypeName,StringComparison.OrdinalIgnoreCase)) + collectionTypeString = StringTypeName; // use the conventional casing if need be + return $"LinkedList<{GetNamespaceName(codeProperty.NamespaceName,apiVersion)}{collectionTypeString}>"; + case PropertyType.Object: + return $"{GetNamespaceName(codeProperty.NamespaceName,apiVersion)}{ReplaceIfReservedName(typeString)}"; + case PropertyType.Map: + return "HashMap"; + case PropertyType.String: + return StringTypeName; + case PropertyType.Enum: + return $"{GetNamespaceName(codeProperty.NamespaceName, apiVersion)}{ReplaceIfReservedName(typeString.Split('.')[0])}"; + case PropertyType.DateOnly: + return "LocalDate"; + case PropertyType.TimeOnly: + return "LocalTime"; + case PropertyType.Guid: + return "UUID"; + case PropertyType.DateTime: + return "OffsetDateTime"; + case PropertyType.Duration: + return "PeriodAndDuration"; + case PropertyType.Binary: + return "ByteArrayInputStream"; + default: + return ReplaceIfReservedName(typeString); + } + } + + private static string GetPropertyObjectName(CodeProperty codeProperty) + { + var propertyName = codeProperty.Name.CleanupSymbolName().ToPascalCase(); + return ReplaceIfReservedName(propertyName); + } + + private static string GetNamespaceName(string namespaceName, string apiVersion) + { + if (string.IsNullOrEmpty(namespaceName) || namespaceName.Equals(ModelNamespacePrefixToTrim, StringComparison.OrdinalIgnoreCase)) + return string.Empty; + + //strip the default namespace name from the original as the models are typically already in Microsoft.Graph namespace + namespaceName = namespaceName.Replace(DefaultNamespace,string.Empty, StringComparison.OrdinalIgnoreCase); + + var normalizedNameSpaceName = namespaceName.TrimStart('.').Split('.',StringSplitOptions.RemoveEmptyEntries) + .Select(static x => ReplaceIfReservedName(x, "Namespace").ToLowerInvariant()) + .Aggregate(static (z, y) => z + '.' + y); + + return $"com.{GetDefaultNamespaceName(apiVersion)}.{normalizedNameSpaceName.Replace("me.", "users.item.")}."; + } + + private static string EnsureJavaVariableNameIsUnique(string variableName, List usedVariableNames) + { + var count = usedVariableNames.Count(x => x.Equals(variableName, StringComparison.OrdinalIgnoreCase)); + usedVariableNames.Add(variableName); + if (count > 0) + { + return $"{variableName}{count}";//append the count to the end of string since we've used it before + } + return variableName; + } + + + private static string GetDefaultNamespaceName(string apiVersion) => + apiVersion.Equals("beta", StringComparison.OrdinalIgnoreCase) ? $"{DefaultNamespace}.beta" : DefaultNamespace; + } +} diff --git a/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs b/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs index 48934742d..b57d58c2e 100644 --- a/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs +++ b/CodeSnippetsReflection.OpenAPI/ModelGraph/SnippetCodeGraph.cs @@ -129,6 +129,11 @@ public Boolean HasParameters() return Parameters.Any(); } + public Boolean HasPathParameters() + { + return PathParameters.Any(); + } + public Boolean HasBody() { return Body.PropertyType != PropertyType.Default; diff --git a/GraphWebApi/Controllers/SnippetsController.cs b/GraphWebApi/Controllers/SnippetsController.cs index d931f7e40..7e95567c3 100644 --- a/GraphWebApi/Controllers/SnippetsController.cs +++ b/GraphWebApi/Controllers/SnippetsController.cs @@ -74,7 +74,7 @@ public async Task PostAsync(string lang = "c#", string generation { // Default to openapi generation if supported and not explicitly requested for. if (string.IsNullOrEmpty(generation)) - generation = OpenApiSnippetsGenerator.SupportedLanguages.Contains(lang) ? "openapi" : "odata"; + generation = (OpenApiSnippetsGenerator.SupportedLanguages.Contains(lang)) ? "openapi" : "odata"; Request.EnableBuffering(); using var streamContent = new StreamContent(Request.Body);