Browse Source

SONAR-21826 Display content-type in APIv2 documentation

tags/10.5.0.89998
Jeremy Davis 1 month ago
parent
commit
b94103d175

+ 4
- 3
server/sonar-web/src/main/js/apps/web-api-v2/WebApiApp.tsx View File

@@ -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() {
<LargeCenteredLayout>
<PageContentFontWrapper className="sw-body-sm">
<Helmet defer={false} title={translate('api_documentation.page')} />
<Spinner loading={isLoading}>
<Spinner isLoading={isLoading}>
{data && (
<div className="sw-w-full sw-flex">
<NavContainer aria-label={translate('api_documentation.page')} className="sw--mx-2">
@@ -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)' }}
>
<Spinner loading={isLoading}>
<Spinner isLoading={isLoading}>
{!activeData && (
<>
<Title>{translate('about')}</Title>

+ 178
- 132
server/sonar-web/src/main/js/apps/web-api-v2/__tests__/utils-test.ts View File

@@ -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);
});
});

+ 2
- 2
server/sonar-web/src/main/js/apps/web-api-v2/components/ApiParameters.tsx View File

@@ -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<OpenAPIV3.OperationObject>;
@@ -153,7 +153,7 @@ export default function ApiParameters({ data }: Readonly<Props>) {
<SubHeading id="api_documentation.v2.request_subheader.request_body">
{translate('api_documentation.v2.request_subheader.request_body')}
</SubHeading>
<ApiResponseSchema content={requestBody} />
<ApiRequestSchema content={requestBody} />
<ApiRequestBodyParameters content={requestBody} />
</div>
)}

+ 49
- 0
server/sonar-web/src/main/js/apps/web-api-v2/components/ApiRequestSchema.tsx View File

@@ -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<ExcludeReferences<OpenAPIV3.ResponseObject>['content'], undefined>;
}

export default function ApiRequestSchema(props: Readonly<Props>) {
const { content, ...other } = props;

const results = extractSchemaAndMediaType(content);

if (results.length === 0) {
return <TextMuted text={translate('no_data')} />;
}

return results.map(({ requestMediaType, schema }) => (
<Card key={requestMediaType}>
<div>
<span>{translate('api_documentation.v2.request_subheader.request_content_type')}</span>
<CodeSnippet snippet={requestMediaType} isOneLine noCopy />
</div>
<CodeSnippet language="json" className="sw-p-6" snippet={schema} wrap="words" {...other} />
</Card>
));
}

+ 11
- 10
server/sonar-web/src/main/js/apps/web-api-v2/components/ApiResponseSchema.tsx View File

@@ -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<HtmlHTMLAttributes<HTMLDivElement>, 'content'> {
interface Props {
content?: Exclude<ExcludeReferences<OpenAPIV3.ResponseObject>['content'], undefined>;
}

export default function ApiResponseSchema(props: Readonly<Props>) {
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 <TextMuted text={translate('no_data')} />;
}

return (
return results.map(({ requestMediaType, schema }) => (
<CodeSnippet
key={requestMediaType}
language="json"
className="sw-p-6"
snippet={JSON.stringify(mapOpenAPISchema(schema), null, 2)}
snippet={schema}
wrap="words"
{...other}
/>
);
));
}

+ 29
- 0
server/sonar-web/src/main/js/apps/web-api-v2/utils.ts View File

@@ -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<ExcludeReferences<OpenAPIV3.ResponseObject>['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;
}

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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}



Loading…
Cancel
Save