-
Notifications
You must be signed in to change notification settings - Fork 12
Adds ToApiManifest extension method for converting an OpenAPI document to an APIManifest
#48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
8d5a758
feat: Adds ToApiManifest extension method.
peombwa f6a784f
Merge branch 'main' into po/AddsGenerationOfApiManifest
peombwa cc798b3
chore: Adds OpenAPI to API Manifest mapping doc.
peombwa 6c751ad
Merge branch 'po/AddsGenerationOfApiManifest' of https://github.com/m…
peombwa 8db5cef
chore: Update changelog.
peombwa 95f68f6
chore: Use constants for default values.
peombwa f31fcfa
chore: Allow customers to provide publisher info.
peombwa 6a4bbf9
chore: Rename source generated REGEX.
peombwa ee957a5
chore: Handle empty contact info.
peombwa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # OpenAPI to API Manifest Mapping | ||
|
|
||
| ## Overview | ||
|
|
||
| This document provides a mapping that's used to convert an OpenAPI document to an API manifest document. An OpenAPI document is a standard format for describing the interface and operations of a web service. An API manifest is a standard that's used to declare an application's HTTP API dependencies and includes links to API descriptions, specifics of HTTP API requests, and related authorization details. | ||
|
|
||
| ## Mapping Diagram | ||
|
|
||
| The following diagram illustrates how an OpenAPI document is mapped to an API manifest document. | ||
|
|
||
| ``` mermaid | ||
| graph LR | ||
| subgraph OpenApiDocument | ||
| A1[Info.Contact.Name] | ||
| A2[Info.Contact.Email] | ||
| A3[Info.Title] | ||
| A4[Servers.Url] | ||
| A5[Info.Version] | ||
| A6[Paths.Key] | ||
| A7[Paths.Operations.Key] | ||
| end | ||
| subgraph ApiManifestDocument | ||
| B1[Publisher.Name] | ||
| B2[Publisher.Email] | ||
| B3[ApiDependencies.Key] | ||
| B4["ApiDependencies[key].ApiDeploymentBaseUrl"] | ||
| B5["ApiDependencies[key].ApiDescriptionVersion"] | ||
| B6["ApiDependencies[key].Requests.UriTemplate"] | ||
| B7["ApiDependencies[key].Requests.Method"] | ||
| end | ||
| A1 -- "( 1 )" --> B1 | ||
| A2 -- "( 2 )" --> B2 | ||
| A3 -- "( 3 )" --> B3 | ||
| A4 -- "( 4 )" --> B4 | ||
| A5 -- "( 5 )" --> B5 | ||
| A6 -- "( 6 )" --> B6 | ||
| A7 -- "( 7 )" --> B7 | ||
| ``` | ||
|
|
||
| ### Mapping Steps | ||
|
|
||
| 1. `Publisher.Name`: If `Info.Contact.Name` is present in the OpenAPI document, it maps to `Publisher.Name` in the API Manifest document. If not, the default value is `publisher-name`. This field is required in the API Manifest. | ||
| 2. `Publisher.Email`: If `Info.Contact.Email` is present in the OpenAPI document, it maps to `Publisher.Email` in the API Manifest document. If not, the default value is `[email protected]`. This field is required in the API Manifest. | ||
| 3. `ApiDependencies.Key`: If a customer doesn't provide a key for an ApiDependency in the API Manifest document, the `Info.Title` from the OpenAPI document is used. The converter modifies the `Info.Title` value by removing any leading or trailing whitespace and replacing any spaces between words with `-` | ||
| 4. `ApiDependencies[key].ApiDeploymentBaseUrl`: If the `Servers` field in the OpenAPI document contains at least one server, the URL of the first server maps to this field in the API Manifest document. If not, this field is assumed to be null. | ||
| 5. `ApiDependencies[key].ApiDescriptionVersion`: The `Info.Version` from the OpenAPI document maps to this field in the API Manifest document. | ||
| 6. `ApiDependencies[key].Requests.UriTemplate`: The `Paths.Key` from the OpenAPI document maps to `Requests.UriTemplate` field in the API Manifest document. | ||
| 7. `ApiDependencies[key].Requests.Method`: The `Paths.Operations.Key` from the OpenAPI document maps to `Requests.Method` field in the API Manifest document. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT license. | ||
|
|
||
| using Microsoft.OpenApi.ApiManifest.Helpers; | ||
| using Microsoft.OpenApi.Models; | ||
|
|
||
| namespace Microsoft.OpenApi.ApiManifest.TypeExtensions | ||
| { | ||
| public static class OpenApiDocumentExtensions | ||
| { | ||
| private const string DefaultPublisherName = "publisher-name"; | ||
| private const string DefaultPublisherEmail = "[email protected]"; | ||
|
|
||
| /// <summary> | ||
| /// Converts an <see cref="OpenApiDocument"/> to an <see cref="ApiManifestDocument"/>. | ||
| /// </summary> | ||
| /// <param name="document">The OpenAPI document to convert.</param> | ||
| /// <param name="apiDescriptionUrl">The URL of the API description.</param> | ||
| /// <param name="applicationName">The name of the application.</param> | ||
| /// <param name="apiDependencyName">The name of the API dependency.</param> | ||
| /// <returns>An <see cref="ApiManifestDocument"/>.</returns> | ||
| public static ApiManifestDocument ToApiManifest(this OpenApiDocument document, string? apiDescriptionUrl, string applicationName, string? apiDependencyName = default) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(document); | ||
| ValidationHelpers.ValidateNullOrWhitespace(nameof(apiDescriptionUrl), apiDescriptionUrl, nameof(ApiManifestDocument)); | ||
| ValidationHelpers.ValidateNullOrWhitespace(nameof(applicationName), applicationName, nameof(ApiManifestDocument)); | ||
|
|
||
| apiDependencyName = NormalizeApiName(apiDependencyName ?? document.Info.Title); | ||
peombwa marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| var publisherName = document.Info.Contact?.Name ?? DefaultPublisherName; | ||
| var publisherEmail = document.Info.Contact?.Email ?? DefaultPublisherEmail; | ||
peombwa marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| string? apiDeploymentBaseUrl = GetApiDeploymentBaseUrl(document.Servers.FirstOrDefault()); | ||
|
|
||
| var apiManifest = new ApiManifestDocument(applicationName) | ||
| { | ||
| Publisher = new(publisherName, publisherEmail), | ||
| ApiDependencies = new() { | ||
| { | ||
| apiDependencyName, new() { | ||
| ApiDescriptionUrl = apiDescriptionUrl, | ||
| ApiDescriptionVersion = document.Info.Version, | ||
| ApiDeploymentBaseUrl = apiDeploymentBaseUrl, | ||
| } | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| foreach (var path in document.Paths) | ||
| { | ||
| foreach (var operation in path.Value.Operations) | ||
| { | ||
| var requestInfo = new RequestInfo | ||
| { | ||
| Method = operation.Key.ToString(), | ||
| UriTemplate = apiDeploymentBaseUrl != default ? path.Key.TrimStart('/') : path.Key | ||
| }; | ||
| apiManifest.ApiDependencies[apiDependencyName].Requests.Add(requestInfo); | ||
| } | ||
| } | ||
| return apiManifest; | ||
| } | ||
|
|
||
| private static string NormalizeApiName(string apiName) | ||
| { | ||
| // Normalize OpenAPI document title to API name by trimming and replacing spaces with dashes. | ||
| return apiName.Trim().Replace(' ', '-'); | ||
peombwa marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| private static string? GetApiDeploymentBaseUrl(OpenApiServer? server) | ||
| { | ||
| if (server is null) | ||
| return null; | ||
|
|
||
| // Ensure the base URL ends with a slash. | ||
| return !server.Url.EndsWith("/", StringComparison.Ordinal) ? $"{server.Url}/" : server.Url; | ||
| } | ||
| } | ||
| } | ||
162 changes: 162 additions & 0 deletions
162
tests/ApiManifest.Tests/TypeExtensions/OpenApiDocumentExtensionsTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| using Microsoft.OpenApi.ApiManifest.TypeExtensions; | ||
| using Microsoft.OpenApi.Models; | ||
|
|
||
| namespace Microsoft.OpenApi.ApiManifest.Tests.TypeExtensions | ||
| { | ||
| public class OpenApiDocumentExtensionsTests | ||
| { | ||
| private readonly OpenApiDocument exampleDocument; | ||
| public OpenApiDocumentExtensionsTests() | ||
| { | ||
| exampleDocument = CreateDocument(); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData(null)] | ||
| [InlineData("")] | ||
| public void ToApiManifestWithNullApiDescriptionUrlThrowsArgumentException(string? apiDescriptionUrl) | ||
| { | ||
| // Arrange | ||
| var document = new OpenApiDocument(); | ||
|
|
||
| // Act | ||
| var exception = Assert.Throws<ArgumentNullException>(() => document.ToApiManifest(apiDescriptionUrl, "application-name")); | ||
|
|
||
| // Assert | ||
| Assert.Equal("apiDescriptionUrl", exception.ParamName); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ToApiManifestWithNullApplicationNameThrowsArgumentException() | ||
| { | ||
| // Arrange | ||
| var document = new OpenApiDocument(); | ||
| var apiDescriptionUrl = "https://example.com/api-description.yaml"; | ||
|
|
||
| // Act | ||
| var exception = Assert.Throws<ArgumentNullException>(() => document.ToApiManifest(apiDescriptionUrl, string.Empty)); | ||
|
|
||
| // Assert | ||
| Assert.Equal("applicationName", exception.ParamName); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ToApiManifestWithValidDocumentReturnsApiManifestDocument() | ||
| { | ||
| // Arrange | ||
| var apiDescriptionUrl = "https://example.com/api-description.yaml"; | ||
| var applicationName = "application-name"; | ||
|
|
||
| // Act | ||
| var apiManifest = exampleDocument.ToApiManifest(apiDescriptionUrl, applicationName); | ||
|
|
||
| // Assert | ||
| Assert.NotNull(apiManifest); | ||
| Assert.Equal(applicationName, apiManifest.ApplicationName); | ||
| Assert.NotNull(apiManifest.Publisher); | ||
| Assert.Equal(exampleDocument.Info.Contact?.Name, apiManifest.Publisher?.Name); | ||
| Assert.Equal(exampleDocument.Info.Contact?.Email, apiManifest.Publisher?.ContactEmail); | ||
| Assert.NotNull(apiManifest.ApiDependencies); | ||
| _ = Assert.Single(apiManifest.ApiDependencies); | ||
| Assert.Equal(exampleDocument.Info.Title.Trim().Replace(' ', '-'), apiManifest.ApiDependencies.First().Key); | ||
| Assert.Equal(apiDescriptionUrl, apiManifest.ApiDependencies.First().Value.ApiDescriptionUrl); | ||
| Assert.Equal(exampleDocument.Info.Version, apiManifest.ApiDependencies.First().Value.ApiDescriptionVersion); | ||
| Assert.Equal(exampleDocument.Servers.First().Url, apiManifest.ApiDependencies.First().Value.ApiDeploymentBaseUrl); | ||
| Assert.Equal(exampleDocument.Paths.Count, apiManifest.ApiDependencies.First().Value.Requests.Count); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ToApiManifestWithValidDocumentAndApiDependencyNameReturnsApiManifestDocument() | ||
| { | ||
| // Arrange | ||
| var apiDescriptionUrl = "https://example.com/api-description.yaml"; | ||
| var applicationName = "application-name"; | ||
| var apiDependencyName = "graph"; | ||
|
|
||
| // Act | ||
| var apiManifest = exampleDocument.ToApiManifest(apiDescriptionUrl, applicationName, apiDependencyName); | ||
|
|
||
| // Assert | ||
| Assert.NotNull(apiManifest); | ||
| Assert.Equal(applicationName, apiManifest.ApplicationName); | ||
| Assert.NotNull(apiManifest.Publisher); | ||
| Assert.Equal(exampleDocument.Info.Contact?.Name, apiManifest.Publisher?.Name); | ||
| Assert.Equal(exampleDocument.Info.Contact?.Email, apiManifest.Publisher?.ContactEmail); | ||
| Assert.NotNull(apiManifest.ApiDependencies); | ||
| _ = Assert.Single(apiManifest.ApiDependencies); | ||
| Assert.Equal(apiDependencyName, apiManifest.ApiDependencies.First().Key); | ||
| Assert.Equal(apiDescriptionUrl, apiManifest.ApiDependencies.First().Value.ApiDescriptionUrl); | ||
| Assert.Equal(exampleDocument.Info.Version, apiManifest.ApiDependencies.First().Value.ApiDescriptionVersion); | ||
| Assert.Equal(exampleDocument.Servers.First().Url, apiManifest.ApiDependencies.First().Value.ApiDeploymentBaseUrl); | ||
| Assert.Equal(exampleDocument.Paths.Count, apiManifest.ApiDependencies.First().Value.Requests.Count); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ToApiManifestWithValidDocumentAndApiDependencyNameAndApiDeploymentBaseUrlReturnsApiManifestDocument() | ||
| { | ||
| // Arrange | ||
| var apiDescriptionUrl = "https://example.com/api-description.yaml"; | ||
| var applicationName = "application-name"; | ||
| var apiDependencyName = "graph"; | ||
| var apiDeploymentBaseUrl = "https://example.com/api/"; | ||
|
|
||
| // Act | ||
| var apiManifest = exampleDocument.ToApiManifest(apiDescriptionUrl, applicationName, apiDependencyName); | ||
|
|
||
| // Assert | ||
| Assert.NotNull(apiManifest); | ||
| Assert.Equal(applicationName, apiManifest.ApplicationName); | ||
| Assert.NotNull(apiManifest.Publisher); | ||
| Assert.Equal(exampleDocument.Info.Contact?.Name, apiManifest.Publisher?.Name); | ||
| Assert.Equal(exampleDocument.Info.Contact?.Email, apiManifest.Publisher?.ContactEmail); | ||
| Assert.NotNull(apiManifest.ApiDependencies); | ||
| _ = Assert.Single(apiManifest.ApiDependencies); | ||
| Assert.Equal(apiDependencyName, apiManifest.ApiDependencies.First().Key); | ||
| Assert.Equal(apiDescriptionUrl, apiManifest.ApiDependencies.First().Value.ApiDescriptionUrl); | ||
| Assert.Equal(exampleDocument.Info.Version, apiManifest.ApiDependencies.First().Value.ApiDescriptionVersion); | ||
| Assert.Equal(apiDeploymentBaseUrl, apiManifest.ApiDependencies.First().Value.ApiDeploymentBaseUrl); | ||
| Assert.Equal(exampleDocument.Paths.Count, apiManifest.ApiDependencies.First().Value.Requests.Count); | ||
| } | ||
|
|
||
| private static OpenApiDocument CreateDocument() | ||
| { | ||
| return new OpenApiDocument | ||
| { | ||
| Info = new OpenApiInfo | ||
| { | ||
| Title = "Graph API", | ||
| Version = "v1.0", | ||
| Contact = new OpenApiContact | ||
| { | ||
| Name = "publisher-name", | ||
| Email = "[email protected]" | ||
| } | ||
| }, | ||
| Servers = new List<OpenApiServer> | ||
| { | ||
| new OpenApiServer | ||
| { | ||
| Url = "https://example.com/api/" | ||
| } | ||
| }, | ||
| Paths = new OpenApiPaths | ||
| { | ||
| ["/users"] = new OpenApiPathItem | ||
| { | ||
| Operations = new Dictionary<OperationType, OpenApiOperation> | ||
| { | ||
| [OperationType.Get] = new OpenApiOperation() | ||
| } | ||
| }, | ||
| ["/groups"] = new OpenApiPathItem | ||
| { | ||
| Operations = new Dictionary<OperationType, OpenApiOperation> | ||
| { | ||
| [OperationType.Get] = new OpenApiOperation() | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| } | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.