From b94103d175321a9b2a447295b42efce0d3f27a67 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Thu, 28 Mar 2024 16:53:12 +0100 Subject: [PATCH] SONAR-21826 Display content-type in APIv2 documentation --- .../src/main/js/apps/web-api-v2/WebApiApp.tsx | 7 +- .../apps/web-api-v2/__tests__/utils-test.ts | 310 ++++++++++-------- .../web-api-v2/components/ApiParameters.tsx | 4 +- .../components/ApiRequestSchema.tsx | 49 +++ .../components/ApiResponseSchema.tsx | 21 +- .../src/main/js/apps/web-api-v2/utils.ts | 29 ++ .../resources/org/sonar/l10n/core.properties | 1 + 7 files changed, 274 insertions(+), 147 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/web-api-v2/components/ApiRequestSchema.tsx diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/WebApiApp.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/WebApiApp.tsx index df1ff1ff3df..c041e68f028 100644 --- a/server/sonar-web/src/main/js/apps/web-api-v2/WebApiApp.tsx +++ b/server/sonar-web/src/main/js/apps/web-api-v2/WebApiApp.tsx @@ -18,7 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import styled from '@emotion/styled'; -import { LargeCenteredLayout, PageContentFontWrapper, Spinner, Title } from 'design-system'; +import { Spinner } from '@sonarsource/echoes-react'; +import { LargeCenteredLayout, PageContentFontWrapper, Title } from 'design-system'; import { omit } from 'lodash'; import React, { useMemo, useState } from 'react'; import { Helmet } from 'react-helmet-async'; @@ -68,7 +69,7 @@ export default function WebApiApp() { - + {data && (
@@ -87,7 +88,7 @@ export default function WebApiApp() { className="sw-relative sw-ml-12 sw-flex-1 sw-overflow-y-auto sw-py-6" style={{ height: 'calc(100vh - 160px)' }} > - + {!activeData && ( <> {translate('about')} diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/web-api-v2/__tests__/utils-test.ts index b7009f9a7fe..cf3c3618b17 100644 --- a/server/sonar-web/src/main/js/apps/web-api-v2/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/web-api-v2/__tests__/utils-test.ts @@ -17,11 +17,89 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { dereferenceSchema, mapOpenAPISchema } from '../utils'; +import { dereferenceSchema, extractSchemaAndMediaType, mapOpenAPISchema } from '../utils'; -it('should dereference schema', () => { - expect( - dereferenceSchema({ +describe('dereferenceSchema', () => { + it('should dereference schema', () => { + expect( + dereferenceSchema({ + openapi: '3.0.1', + info: { + title: 'SonarQube Web API', + version: '1.0.0 beta', + }, + paths: { + '/test': { + delete: { + responses: { + '200': { + description: 'Internal Server Error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Test', + }, + }, + }, + }, + }, + }, + }, + '/test/{first}': { + get: { + parameters: [ + { + name: 'first', + in: 'path', + description: '1', + schema: { + $ref: '#/components/schemas/NestedTest', + }, + }, + { + name: 'second', + in: 'query', + description: '2', + schema: { + type: 'string', + }, + }, + ], + responses: { + '200': { + description: 'Internal Server Error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/NestedTest', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + NestedTest: { + type: 'object', + properties: { + test: { + type: 'array', + items: { + $ref: '#/components/schemas/Test', + }, + }, + }, + }, + Test: { + type: 'string', + }, + }, + }, + }), + ).toStrictEqual({ openapi: '3.0.1', info: { title: 'SonarQube Web API', @@ -36,7 +114,7 @@ it('should dereference schema', () => { content: { 'application/json': { schema: { - $ref: '#/components/schemas/Test', + type: 'string', }, }, }, @@ -52,7 +130,15 @@ it('should dereference schema', () => { in: 'path', description: '1', schema: { - $ref: '#/components/schemas/NestedTest', + type: 'object', + properties: { + test: { + type: 'array', + items: { + type: 'string', + }, + }, + }, }, }, { @@ -70,7 +156,15 @@ it('should dereference schema', () => { content: { 'application/json': { schema: { - $ref: '#/components/schemas/NestedTest', + type: 'object', + properties: { + test: { + type: 'array', + items: { + type: 'string', + }, + }, + }, }, }, }, @@ -87,7 +181,7 @@ it('should dereference schema', () => { test: { type: 'array', items: { - $ref: '#/components/schemas/Test', + type: 'string', }, }, }, @@ -97,143 +191,95 @@ it('should dereference schema', () => { }, }, }, - }), - ).toStrictEqual({ - openapi: '3.0.1', - info: { - title: 'SonarQube Web API', - version: '1.0.0 beta', - }, - paths: { - '/test': { - delete: { - responses: { - '200': { - description: 'Internal Server Error', - content: { - 'application/json': { - schema: { - type: 'string', - }, - }, - }, - }, + }); + }); +}); + +describe('mapOpenAPISchema', () => { + it('should map open api response schema', () => { + expect( + mapOpenAPISchema({ + type: 'object', + properties: { + str: { + type: 'string', }, - }, - }, - '/test/{first}': { - get: { - parameters: [ - { - name: 'first', - in: 'path', - description: '1', - schema: { - type: 'object', - properties: { - test: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - }, - }, - { - name: 'second', - in: 'query', - description: '2', - schema: { - type: 'string', - }, - }, - ], - responses: { - '200': { - description: 'Internal Server Error', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - }, - }, - }, - }, + int: { + type: 'integer', + format: 'int32', }, - }, - }, - }, - components: { - schemas: { - NestedTest: { - type: 'object', - properties: { - test: { - type: 'array', - items: { - type: 'string', - }, - }, + num: { + type: 'number', + format: 'double', + }, + bool: { + type: 'boolean', }, }, - Test: { + }), + ).toStrictEqual({ + str: 'string', + int: 'integer (int32)', + num: 'number (double)', + bool: 'boolean', + }); + + expect( + mapOpenAPISchema({ + type: 'array', + items: { type: 'string', }, - }, - }, + }), + ).toStrictEqual(['string']); + + expect( + mapOpenAPISchema({ + type: 'string', + enum: ['GREEN', 'YELLOW', 'RED'], + }), + ).toStrictEqual('Enum (string): GREEN, YELLOW, RED'); }); }); -it('should map open api response schema', () => { - expect( - mapOpenAPISchema({ - type: 'object', - properties: { - str: { - type: 'string', - }, - int: { - type: 'integer', - format: 'int32', - }, - num: { - type: 'number', - format: 'double', +describe('extractSchemaAndMediaType', () => { + it('should extract the schema', () => { + const result = extractSchemaAndMediaType({ + 'application/json': { + schema: { + type: 'object', + properties: { + name: { type: 'string', description: 'username' }, + age: { type: 'number', description: 'age', minimum: 0, maximum: 130 }, + }, }, - bool: { - type: 'boolean', + }, + 'application/merge-patch+json': { + schema: { + type: 'object', + properties: { + name: { type: 'string', description: 'username' }, + age: { type: 'number', description: 'age', minimum: 0, maximum: 130 }, + }, }, }, - }), - ).toStrictEqual({ - str: 'string', - int: 'integer (int32)', - num: 'number (double)', - bool: 'boolean', + }); + + expect(result).toHaveLength(2); }); - expect( - mapOpenAPISchema({ - type: 'array', - items: { - type: 'string', - }, - }), - ).toStrictEqual(['string']); + it('should handle missing schema', () => { + const result = extractSchemaAndMediaType({ + 'application/json': {}, + 'application/merge-patch+json': {}, + }); + + expect(result).toHaveLength(0); + }); - expect( - mapOpenAPISchema({ - type: 'string', - enum: ['GREEN', 'YELLOW', 'RED'], - }), - ).toStrictEqual('Enum (string): GREEN, YELLOW, RED'); + it('should handle no content', () => { + const result = extractSchemaAndMediaType(); + + expect(result).toHaveLength(0); + }); }); diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiParameters.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiParameters.tsx index e0b3657da16..1e79da3a816 100644 --- a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiParameters.tsx +++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiParameters.tsx @@ -27,7 +27,7 @@ import { ExcludeReferences, InternalExtension } from '../types'; import { mapOpenAPISchema } from '../utils'; import ApiFilterContext from './ApiFilterContext'; import ApiRequestBodyParameters from './ApiRequestParameters'; -import ApiResponseSchema from './ApiResponseSchema'; +import ApiRequestSchema from './ApiRequestSchema'; interface Props { data: ExcludeReferences; @@ -153,7 +153,7 @@ export default function ApiParameters({ data }: Readonly) { {translate('api_documentation.v2.request_subheader.request_body')} - +
)} diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiRequestSchema.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiRequestSchema.tsx new file mode 100644 index 00000000000..b7a11e01988 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiRequestSchema.tsx @@ -0,0 +1,49 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { Card, CodeSnippet, TextMuted } from 'design-system'; +import { OpenAPIV3 } from 'openapi-types'; +import React from 'react'; +import { translate } from '../../../helpers/l10n'; +import { ExcludeReferences } from '../types'; +import { extractSchemaAndMediaType } from '../utils'; + +interface Props { + content: Exclude['content'], undefined>; +} + +export default function ApiRequestSchema(props: Readonly) { + const { content, ...other } = props; + + const results = extractSchemaAndMediaType(content); + + if (results.length === 0) { + return ; + } + + return results.map(({ requestMediaType, schema }) => ( + +
+ {translate('api_documentation.v2.request_subheader.request_content_type')} + +
+ +
+ )); +} diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiResponseSchema.tsx b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiResponseSchema.tsx index 2fca7f158c3..19cce2011ba 100644 --- a/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiResponseSchema.tsx +++ b/server/sonar-web/src/main/js/apps/web-api-v2/components/ApiResponseSchema.tsx @@ -19,31 +19,32 @@ */ import { CodeSnippet, TextMuted } from 'design-system'; import { OpenAPIV3 } from 'openapi-types'; -import React, { HtmlHTMLAttributes } from 'react'; +import React from 'react'; import { translate } from '../../../helpers/l10n'; import { ExcludeReferences } from '../types'; -import { mapOpenAPISchema } from '../utils'; +import { extractSchemaAndMediaType } from '../utils'; -interface Props extends Omit, 'content'> { +interface Props { content?: Exclude['content'], undefined>; } export default function ApiResponseSchema(props: Readonly) { const { content, ...other } = props; - const schema = - content && - (content['application/json']?.schema || content['application/merge-patch+json']?.schema); - if (!schema) { + + const results = extractSchemaAndMediaType(content); + + if (results.length === 0) { return ; } - return ( + return results.map(({ requestMediaType, schema }) => ( - ); + )); } diff --git a/server/sonar-web/src/main/js/apps/web-api-v2/utils.ts b/server/sonar-web/src/main/js/apps/web-api-v2/utils.ts index 7e8826d05f9..fd139a638a6 100644 --- a/server/sonar-web/src/main/js/apps/web-api-v2/utils.ts +++ b/server/sonar-web/src/main/js/apps/web-api-v2/utils.ts @@ -19,6 +19,7 @@ */ import { mapValues } from 'lodash'; import { OpenAPIV3 } from 'openapi-types'; +import { isDefined } from '../../helpers/types'; import { DereferenceRecursive, ExcludeReferences } from './types'; export const URL_DIVIDER = '--'; @@ -89,3 +90,31 @@ export const getResponseCodeClassName = (code: string): string => { }; export const getApiEndpointKey = (name: string, method: string) => `${name}${URL_DIVIDER}${method}`; + +const DISPLAYED_MEDIA_TYPES = ['application/json', 'application/merge-patch+json']; + +export function extractSchemaAndMediaType( + content?: Exclude['content'], undefined>, +) { + if (!content) { + return []; + } + + const requests = Object.keys(content) + .filter((mediaType) => DISPLAYED_MEDIA_TYPES.includes(mediaType)) + .map((requestMediaType) => { + const schema = content[requestMediaType]?.schema; + + if (!schema) { + return null; + } + + return { + requestMediaType, + schema: JSON.stringify(mapOpenAPISchema(schema), null, 2), + }; + }) + .filter(isDefined); + + return requests; +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index ae13b51351a..39fffc2a5b7 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -4106,6 +4106,7 @@ api_documentation.v2.request_subheader.query=Query Parameters api_documentation.v2.request_subheader.path=Path Parameters api_documentation.v2.request_subheader.header=Headers api_documentation.v2.request_subheader.request_body=Request Body +api_documentation.v2.request_subheader.request_content_type=Content-Type: api_documentation.v2.enum_description=Valid values: {values} -- 2.39.5