-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #48 from microsoft/po/AddsGenerationOfApiManifest
Adds `ToApiManifest` extension method for converting an OpenAPI document to an APIManifest
- Loading branch information
Showing
5 changed files
with
370 additions
and
2 deletions.
There are no files selected for viewing
This file contains 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 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 a customer does not provide the publisher name, the `Info.Contact.Name` from the OpenAPI document is used as the `Publisher.Name` in the API Manifest document. If the OpenAPI document does not contain `Info.Contact.Name`, a default value of `publisher-name` is used. This field is required in the API Manifest. | ||
2. `Publisher.Email`: If a customer does not provide the publisher email, the `Info.Contact.Email` from the OpenAPI document is used as the `Publisher.Email` in the API Manifest document. If the OpenAPI document does not contain `Info.Contact.Email`, a default value of `publisher-email@example.com` is used. 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 as the api dependency key. The converter normalizes the `Info.Title` value by removing all special characters and whitespace. | ||
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 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,94 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT license. | ||
|
||
using Microsoft.OpenApi.ApiManifest.Helpers; | ||
using Microsoft.OpenApi.Models; | ||
using System.Text.RegularExpressions; | ||
|
||
namespace Microsoft.OpenApi.ApiManifest.TypeExtensions | ||
{ | ||
public static partial class OpenApiDocumentExtensions | ||
{ | ||
[GeneratedRegex("[^a-zA-Z0-9]", RegexOptions.Compiled, 5000)] | ||
private static partial Regex SpecialCharactersInApiNameRegex(); | ||
internal const string DefaultPublisherName = "publisher-name"; | ||
internal const string DefaultPublisherEmail = "publisher-email@example.com"; | ||
|
||
/// <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. If not specified, it defaults to the title from the OpenAPI document.</param> | ||
/// <param name="publisherName"> | ||
/// The publisher's name for the API manifest. | ||
/// If not provided, it defaults to the contact name from the OpenAPI document (if available). | ||
/// If the contact name is also not available, it defaults to 'publisher-name'. | ||
/// </param> | ||
/// <param name="publisherEmail"> | ||
/// The publisher's email for the API manifest. | ||
/// If not provided, it defaults to the contact email from the OpenAPI document (if available). | ||
/// If the contact email is also not available, it defaults to 'publisher-email@example.com'. | ||
/// </param> | ||
/// <returns>An <see cref="ApiManifestDocument"/>.</returns> | ||
public static ApiManifestDocument ToApiManifest(this OpenApiDocument document, string? apiDescriptionUrl, string applicationName, string? apiDependencyName = default, string? publisherName = default, string? publisherEmail = default) | ||
{ | ||
ArgumentNullException.ThrowIfNull(document); | ||
ValidationHelpers.ValidateNullOrWhitespace(nameof(apiDescriptionUrl), apiDescriptionUrl, nameof(ApiManifestDocument)); | ||
ValidationHelpers.ValidateNullOrWhitespace(nameof(applicationName), applicationName, nameof(ApiManifestDocument)); | ||
|
||
if (string.IsNullOrEmpty(publisherName)) | ||
publisherName = document.Info.Contact?.Name is string cName && !string.IsNullOrEmpty(cName) ? cName : DefaultPublisherName; | ||
|
||
if (string.IsNullOrEmpty(publisherEmail)) | ||
publisherEmail = document.Info.Contact?.Email is string cEmail && !string.IsNullOrEmpty(cEmail) ? cEmail : DefaultPublisherEmail; | ||
|
||
apiDependencyName = NormalizeApiName(string.IsNullOrEmpty(apiDependencyName) ? document.Info.Title : apiDependencyName); | ||
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 dependency name by removing all special characters from the provided api name. | ||
return SpecialCharactersInApiNameRegex().Replace(apiName, string.Empty); | ||
} | ||
|
||
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; | ||
} | ||
} | ||
} |
This file contains 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
220 changes: 220 additions & 0 deletions
220
tests/ApiManifest.Tests/TypeExtensions/OpenApiDocumentExtensionsTests.cs
This file contains 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,220 @@ | ||
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("GraphAPI", 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); | ||
} | ||
|
||
[Fact] | ||
public void ToApiManifestWhenOpenApiContactInfoIsNullAndNoPublisherInfoIIsProvidedReturnsApiManifestDocument() | ||
{ | ||
// Arrange | ||
var apiDescriptionUrl = "https://example.com/api-description.yaml"; | ||
var applicationName = "application-name"; | ||
var apiDependencyName = "graph"; | ||
var apiDeploymentBaseUrl = "https://example.com/api/"; | ||
var localExampleDocument = CreateDocument(); | ||
localExampleDocument.Info.Contact = null; | ||
|
||
// Act | ||
var apiManifest = localExampleDocument.ToApiManifest(apiDescriptionUrl, applicationName, apiDependencyName); | ||
|
||
// Assert | ||
Assert.NotNull(apiManifest); | ||
Assert.Equal(applicationName, apiManifest.ApplicationName); | ||
Assert.NotNull(apiManifest.Publisher); | ||
Assert.Equal(OpenApiDocumentExtensions.DefaultPublisherName, apiManifest.Publisher?.Name); | ||
Assert.Equal(OpenApiDocumentExtensions.DefaultPublisherEmail, 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(localExampleDocument.Info.Version, apiManifest.ApiDependencies.First().Value.ApiDescriptionVersion); | ||
Assert.Equal(apiDeploymentBaseUrl, apiManifest.ApiDependencies.First().Value.ApiDeploymentBaseUrl); | ||
Assert.Equal(localExampleDocument.Paths.Count, apiManifest.ApiDependencies.First().Value.Requests.Count); | ||
} | ||
|
||
[Fact] | ||
public void ToApiManifestWithAllParametersReturnsApiManifestDocument() | ||
{ | ||
// Arrange | ||
var apiDescriptionUrl = "https://example.com/api-description.yaml"; | ||
var applicationName = "application-name"; | ||
var apiDependencyName = "graph"; | ||
var apiDeploymentBaseUrl = "https://example.com/api/"; | ||
var publisherName = "FooBar"; | ||
var publisherEmail = "FooBar@contoso.com"; | ||
|
||
// Act | ||
var apiManifest = exampleDocument.ToApiManifest(apiDescriptionUrl, applicationName, apiDependencyName, publisherName, publisherEmail); | ||
|
||
// Assert | ||
Assert.NotNull(apiManifest); | ||
Assert.Equal(applicationName, apiManifest.ApplicationName); | ||
Assert.NotNull(apiManifest.Publisher); | ||
Assert.Equal(publisherName, apiManifest.Publisher?.Name); | ||
Assert.Equal(publisherEmail, 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 = "foo@bar.com" | ||
} | ||
}, | ||
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() | ||
} | ||
} | ||
} | ||
}; | ||
} | ||
} | ||
} |