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