Skip to content

Commit

Permalink
OAI2-> OAI3: Fail if there is no produces provided for a path and the…
Browse files Browse the repository at this point in the history
… response has a schema/body (#144)
  • Loading branch information
timotheeguerin committed Jan 15, 2021
1 parent 5fabda5 commit 4a39dcc
Show file tree
Hide file tree
Showing 14 changed files with 202 additions and 61 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,4 @@ package-deps.json
**/dist/*
node_modules/*
package-lock.json
common/config/rush/shrinkwrap.yaml
common/config/rush/pnpm-lock.yaml
3 changes: 3 additions & 0 deletions oai2-to-oai3/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Change Log - @azure-tools/oai2-to-oai3

# 4.2.0
- **fix**: Fail if a response has no produces defined and no global defined either [PR 144](https://github.com/Azure/perks/pull/144)

# 4.1.0
- **fix**: Issue with cross-file referneces of body parameters [PR 131](https://github.com/Azure/perks/pull/131)

Expand Down
2 changes: 1 addition & 1 deletion oai2-to-oai3/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@azure-tools/oai2-to-oai3",
"version": "4.1.0",
"version": "4.2.0",
"patchOffset": 100,
"description": "OpenAPI2 to OpenAPI3 conversion library that maintains souremaps for use with AutoRest",
"main": "./dist/src/index.js",
Expand Down
40 changes: 40 additions & 0 deletions oai2-to-oai3/src/content-type-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { OpenAPI2Operation } from "./oai2";

/**
* Resolve the list of content types produced by an operation.
* @param operationProduces: List of content type produces by the operation.
* @param globalProduces: List of default content type produced by the API.
* @returns list of produced content types. Array will have at least one entry.
*/
export const resolveOperationProduces = (
operation: OpenAPI2Operation,
globalProduces: string[],
): string[] => {
const operationProduces = operation.produces;
const produces = operationProduces
? operationProduces.indexOf("application/json") !== -1
? operationProduces
: [...new Set([...operationProduces])]
: globalProduces;

// default
if (produces.length === 0) {
produces.push("*/*");
}

return produces;
};

/**
* Resolve the list of content types consumed by an operation.
* @param operationConsumes: List of content type consumed by the operation.
* @param globalConsumes: List of default content type consumed by the API.
*/
export const resolveOperationConsumes = (operation: OpenAPI2Operation, globalConsumes: string[]): string[] => {
const operationConsumes = operation.consumes;
return operationConsumes
? operationConsumes.indexOf("application/octet-stream") !== -1
? operationConsumes
: [...new Set([...operationConsumes])]
: globalConsumes;
};
31 changes: 14 additions & 17 deletions oai2-to-oai3/src/converter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { createGraphProxy, JsonPointer, Node, visit, get } from '@azure-tools/datastore';
import { Mapping } from 'source-map';
import { resolveOperationConsumes, resolveOperationProduces } from './content-type-utils';
import { OpenAPI2Document, OpenAPI2Header, OpenAPI2Operation, OpenAPI2OperationResponse } from './oai2';
import { cleanElementName, convertOai2RefToOai3, parseOai2Ref } from './refs-utils';
import { ResolveReferenceFn } from './runner';
import { statusCodes } from './status-codes';
Expand All @@ -11,7 +13,7 @@ export class Oai2ToOai3 {
public mappings = new Array<Mapping>();


constructor(protected originalFilename: string, protected original: any, private resolveExternalReference?: ResolveReferenceFn) {
constructor(protected originalFilename: string, protected original: OpenAPI2Document, private resolveExternalReference?: ResolveReferenceFn) {
this.generated = createGraphProxy(this.originalFilename, '', this.mappings);
}

Expand Down Expand Up @@ -697,7 +699,7 @@ export class Oai2ToOai3 {
}
}

async visitOperation(pathItem: any, httpMethod: string, jsonPointer: JsonPointer, operationItemMembers: Iterable<Node>, operationValue: any, globalConsumes: Array<string>, globalProduces: Array<string>) {
async visitOperation(pathItem: any, httpMethod: string, jsonPointer: JsonPointer, operationItemMembers: Iterable<Node>, operationValue: OpenAPI2Operation, globalConsumes: Array<string>, globalProduces: Array<string>) {

// trace was not supported on OpenAPI 2.0, it was an extension
httpMethod = (httpMethod !== 'x-trace') ? httpMethod : 'trace';
Expand All @@ -707,20 +709,9 @@ export class Oai2ToOai3 {
const operation = pathItem[httpMethod];

// preprocess produces and consumes for responses and parameters respectively;
const produces = (operationValue.produces) ?
(operationValue.produces.indexOf('application/json') !== -1) ?
operationValue.produces :
[...new Set([...operationValue.produces])] :
globalProduces;

// default
if (produces.length === 0) {
produces.push('*/*');
}
const produces = resolveOperationProduces(operationValue, globalProduces);
const consumes = resolveOperationConsumes(operationValue, globalConsumes);

const consumes = (operationValue.consumes) ? (operationValue.consumes.indexOf('application/octet-stream') !== -1) ? operationValue.consumes
: [...new Set([...operationValue.consumes])]
: globalConsumes;
for (const { value, key, pointer, children: operationFieldItemMembers } of operationItemMembers) {
switch (key) {
case 'tags':
Expand Down Expand Up @@ -1055,7 +1046,7 @@ export class Oai2ToOai3 {
}
}

async visitResponse(responseTarget: any, responseValue: any, responseName: string, responsesFieldMembers: () => Iterable<Node>, jsonPointer: string, produces: Array<string>) {
async visitResponse(responseTarget: any, responseValue: OpenAPI2OperationResponse, responseName: string, responsesFieldMembers: () => Iterable<Node>, jsonPointer: string, produces: Array<string>) {

// NOTE: The previous converter patches the description of the response because
// every response should have a description.
Expand All @@ -1071,6 +1062,12 @@ export class Oai2ToOai3 {
}

if (responseValue.schema) {
if (produces.length === 0 || (produces.length === 1 && produces[0] === "*/*")) {
throw new Error(
`Operation response '${jsonPointer}' produces type couldn't be resolved. Operation is probably is missing a produces field and there isn't any global value. Please add "produces": [<contentType>]"`,
);
}

responseTarget.content = this.newObject(jsonPointer);
for (const mimetype of produces) {
responseTarget.content[mimetype] = this.newObject(jsonPointer);
Expand Down Expand Up @@ -1151,7 +1148,7 @@ export class Oai2ToOai3 {
'uniqueItems'
];

async visitHeader(targetHeader: any, headerValue: any, jsonPointer: string) {
async visitHeader(targetHeader: any, headerValue: OpenAPI2Header, jsonPointer: string) {
if (headerValue.$ref) {
targetHeader.$ref = { value: this.convertReferenceToOai3(headerValue.schema.$ref), pointer: jsonPointer };
} else {
Expand Down
33 changes: 33 additions & 0 deletions oai2-to-oai3/src/oai2/definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export interface OpenAPI2Definition {
[key: string]: unknown;

additionalProperties?: OpenAPI2Definition | OpenAPI2Reference | boolean;
allOf?: OpenAPI2Definition[];
description?: string;
enum?: string[];
format?: string;
items?: OpenAPI2Definition | OpenAPI2Reference;
oneOf?: (OpenAPI2Definition | OpenAPI2Reference)[];
properties?: { [index: string]: OpenAPI2Definition | OpenAPI2Reference };
required?: string[];
title?: string;
type?: OpenAPI2Type; // allow this to be optional to cover cases when this is missing
}

export interface OpenAPI2Reference {
$ref: string;
}

export type OpenAPI2Type =
| "array"
| "boolean"
| "byte"
| "date"
| "dateTime"
| "double"
| "float"
| "integer"
| "long"
| "number"
| "object"
| "string";
19 changes: 19 additions & 0 deletions oai2-to-oai3/src/oai2/document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { OpenAPI2Definition } from "./definition";
import { OpenAPI2Path } from "./paths";

export interface OpenAPI2Document {
swagger: "2.0";
produces?: string[];
consumes?: string[];
schemes?: string[];
host?: string;
basePath?: string;
paths: { [path: string]: OpenAPI2Path };
definitions: { [name: string]: OpenAPI2Definition };
}

export interface OpenAPI2DocumentInfo {
title: string;
description: string;
version: string;
}
3 changes: 3 additions & 0 deletions oai2-to-oai3/src/oai2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./document";
export * from "./paths";
export * from "./definition";
42 changes: 42 additions & 0 deletions oai2-to-oai3/src/oai2/paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { OpenAPI2Definition, OpenAPI2Reference } from "./definition";

/**
* Custom http method verb for autorest.
*/
export type HttpMethodCustom = "x-trace";

export type HttpMethod = "get" | "post" | "patch" | "put" | "delete" | "options" | "head" | "trace";

export type OpenAPI2Path = {
[method in HttpMethod]: OpenAPI2Path;
} & {
parameters: any[];
};

export interface OpenAPI2Operation {
operationId: string;
description: string;
responses: OpenAPI2OperationResponses;
parameters?: any[];
produces?: string[];
consumes?: string[];
}

export type OpenAPI2OperationResponses = { [statusCode: string]: OpenAPI2OperationResponse };

export interface OpenAPI2OperationResponse {
description: string;
schema?: OpenAPI2Definition;
examples?: { [exampleName: string]: unknown };
headers?: { [headerName: string]: OpenAPI2Header };
}

export type OpenAPI2Header = OpenAPI2Reference & OpenAPI2HeaderDefinition;

export interface OpenAPI2HeaderDefinition {
type: "string" | "number" | "integer" | "boolean" | "array";
schema: OpenAPI2Reference & OpenAPI2Definition;
description: string;
items: any;
collectionFormat: "csv" | "ssv" | "tsv" | "pipes" | "multi";
}
3 changes: 2 additions & 1 deletion oai2-to-oai3/src/runner/oai2-to-oai3-runner.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { DataHandle, get } from "@azure-tools/datastore";
import { Oai2ToOai3 } from "../converter";
import { OpenAPI2Document } from "../oai2";
import { loadInputFiles } from "./utils";

export interface OaiToOai3FileInput {
name: string;
schema: any; // OAI2 type?
schema: OpenAPI2Document; // OAI2 type?
}

export interface OaiToOai3FileOutput {
Expand Down
3 changes: 2 additions & 1 deletion oai2-to-oai3/src/runner/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { DataHandle } from '@azure-tools/datastore';
import { OpenAPI2Document } from '../oai2';
import { OaiToOai3FileInput } from './oai2-to-oai3-runner';

export const loadInputFiles = async (inputFiles: DataHandle[]): Promise<OaiToOai3FileInput[]> => {
const inputs: OaiToOai3FileInput[] = [];
for (const file of inputFiles) {
const schema = await file.ReadObject();
const schema = await file.ReadObject<OpenAPI2Document>();
inputs.push({ name: file.originalFullPath, schema });
}
return inputs;
Expand Down
23 changes: 12 additions & 11 deletions oai2-to-oai3/test/resources/conversion/oai2/exec-service.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"title": "ExecService",
"description": "Azure Dev Spaces ExecService v3.2"
},

"paths": {
"/api/v3.2/AzdsTraces": {
"post": {
Expand All @@ -13,7 +14,7 @@
],
"operationId": "AzdsTraces_PostAzdsUserClusterLogsAsync",
"consumes": [],
"produces": [],
"produces": ["application/json"],
"parameters": [
{
"name": "onbehalfof",
Expand Down Expand Up @@ -54,7 +55,7 @@
"text/json",
"application/*+json"
],
"produces": [],
"produces": ["application/json"],
"parameters": [
{
"name": "spaceName",
Expand Down Expand Up @@ -151,7 +152,7 @@
],
"operationId": "Liveness_LivenessCheck",
"consumes": [],
"produces": [],
"produces": ["application/json"],
"parameters": [],
"responses": {
"200": {
Expand All @@ -173,7 +174,7 @@
],
"operationId": "Ping_CheckUserAuth",
"consumes": [],
"produces": [],
"produces": ["application/json"],
"parameters": [],
"responses": {
"200": {
Expand Down Expand Up @@ -201,7 +202,7 @@
],
"operationId": "Ping_CheckWhitelistAndUserAuth",
"consumes": [],
"produces": [],
"produces": ["application/json"],
"parameters": [],
"responses": {
"200": {
Expand Down Expand Up @@ -229,7 +230,7 @@
],
"operationId": "Ping_CheckWhitelistOnlyAuth",
"consumes": [],
"produces": [],
"produces": ["application/json"],
"parameters": [],
"responses": {
"200": {
Expand Down Expand Up @@ -286,7 +287,7 @@
],
"operationId": "Spaces_CreateSpace",
"consumes": [],
"produces": [],
"produces": ["application/json"],
"parameters": [
{
"name": "spaceName",
Expand Down Expand Up @@ -325,7 +326,7 @@
],
"operationId": "Spaces_DeleteSpace",
"consumes": [],
"produces": [],
"produces": ["application/json"],
"parameters": [
{
"name": "spaceName",
Expand Down Expand Up @@ -374,7 +375,7 @@
],
"operationId": "Spaces_CreateSpaceFromNamespace",
"consumes": [],
"produces": [],
"produces": ["application/json"],
"parameters": [
{
"name": "namespaceName",
Expand Down Expand Up @@ -454,7 +455,7 @@
],
"operationId": "Traces_ReportRequestAsync",
"consumes": [],
"produces": [],
"produces": ["application/json"],
"parameters": [],
"responses": {
"200": {
Expand Down Expand Up @@ -482,7 +483,7 @@
],
"operationId": "Traces_ReportResponseAsync",
"consumes": [],
"produces": [],
"produces": ["application/json"],
"parameters": [],
"responses": {
"200": {
Expand Down
Loading

0 comments on commit 4a39dcc

Please sign in to comment.