Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 92 additions & 6 deletions libs/converter-openapi/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as ADCSDK from '@api7/adc-sdk';
import { dereference, upgrade } from '@scalar/openapi-parser';
import { OpenAPIV3_1 } from '@scalar/openapi-types';
import { isEmpty, unset } from 'lodash-es';
import { Observable, from, map, of, switchMap, tap } from 'rxjs';
import { Observable, defer, from, map, of, switchMap, tap } from 'rxjs';
import slugify from 'slugify';
import { z } from 'zod';

Expand All @@ -22,6 +22,87 @@ const httpMethods: Array<OpenAPIV3_1.HttpMethods> = [
'trace',
];

type UnknownRecord = Record<string, unknown>;

const copiedExtensionKeys = [
ExtKey.NAME,
ExtKey.LABELS,
ExtKey.PLUGINS,
ExtKey.SERVICE_DEFAULTS,
ExtKey.UPSTREAM_DEFAULTS,
ExtKey.UPSTREAM_NODE_DEFAULTS,
ExtKey.ROUTE_DEFAULTS,
];

const isRecord = (value: unknown): value is UnknownRecord =>
typeof value === 'object' && value !== null && !Array.isArray(value);

const copyKey = (source: UnknownRecord, target: UnknownRecord, key: string) => {
if (source[key] !== undefined) target[key] = source[key];
};

const copyExtensionKeys = (source: UnknownRecord, target: UnknownRecord) => {
copiedExtensionKeys.forEach((key) => copyKey(source, target, key));
Object.entries(source)
.filter(([key]) => key.startsWith(ExtKey.PLUGIN_PREFIX))
.forEach(([key, value]) => {
target[key] = value;
});
};

const pruneOperation = (operation: unknown): unknown => {
if (!isRecord(operation)) return operation;

const result: UnknownRecord = {};
['$ref', 'operationId', 'summary', 'description', 'servers'].forEach((key) =>
copyKey(operation, result, key),
);
copyExtensionKeys(operation, result);
return result;
};

const prunePathItem = (pathItem: unknown): unknown => {
if (!isRecord(pathItem)) return pathItem;

const result: UnknownRecord = {};
['$ref', 'servers'].forEach((key) => copyKey(pathItem, result, key));
copyExtensionKeys(pathItem, result);
httpMethods.forEach((method) => {
if (pathItem[method] !== undefined)
result[method] = pruneOperation(pathItem[method]);
});
return result;
};

const prunePathItems = (pathItems: UnknownRecord): UnknownRecord =>
Object.fromEntries(
Object.entries(pathItems).map(([path, pathItem]) => [
path,
prunePathItem(pathItem),
]),
);

const createConversionDocument = (
specification: unknown,
): OpenAPIV3_1.Document => {
const source = specification as unknown as UnknownRecord;
const result: UnknownRecord = {};

['openapi', 'info', 'servers'].forEach((key) => copyKey(source, result, key));
copyExtensionKeys(source, result);

if (isRecord(source.paths)) result.paths = prunePathItems(source.paths);

const components = source.components;
if (isRecord(components) && isRecord(components.pathItems)) {
result.components = {
pathItems: prunePathItems(components.pathItems),
};
}

return result as unknown as OpenAPIV3_1.Document;
};

export class OpenAPIConverter implements ADCSDK.Converter {
public toADC(content: string): Observable<ADCSDK.Configuration> {
return from(this.parseOAS(content)).pipe(
Expand Down Expand Up @@ -135,9 +216,9 @@ export class OpenAPIConverter implements ADCSDK.Converter {
tap((services) =>
services.map((service) => {
if (!service.path_prefix) return service;
service.routes = service.routes!.map((route) => {
service.routes = service.routes!.map((route: ADCSDK.Route) => {
route.uris = route.uris.map(
(uri) => `${service.path_prefix}${uri}`,
(uri: string) => `${service.path_prefix}${uri}`,
);
return route;
});
Expand All @@ -157,7 +238,13 @@ export class OpenAPIConverter implements ADCSDK.Converter {
}

private parseOAS(content: string): Observable<OpenAPIV3_1.Document> {
return of(dereference(content)).pipe(
return defer(() => {
const upgraded = upgrade(content);
if (!upgraded.specification)
throw new Error('No schema found in OpenAPI document');
return of(createConversionDocument(upgraded.specification));
}).pipe(
map((specification) => dereference(specification)),
map((res) => {
if (res.errors?.length)
throw new Error(
Expand All @@ -166,9 +253,8 @@ export class OpenAPIConverter implements ADCSDK.Converter {
.join(', ')}`,
);
if (!res.schema) throw new Error('No schema found in OpenAPI document');
return res.schema;
return res.schema as OpenAPIV3_1.Document;
}),
map((schema) => upgrade(schema).specification),
tap((specification) => {
const result = schema.safeParse(specification);

Expand Down
48 changes: 48 additions & 0 deletions libs/converter-openapi/test/assets/basic-8.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
openapi: 3.0.2
info:
title: SectorAPI
version: 3.0.0
servers:
- url: http://localhost:8080
paths:
/v3/sectors:
get:
operationId: getSectors
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Section'
components:
schemas:
Sector:
type: object
properties:
key:
type: string
description:
type: string
SectorNode:
type: object
properties:
sector:
$ref: '#/components/schemas/Sector'
children:
type: array
items:
$ref: '#/components/schemas/SectorNode'
Section:
type: object
properties:
key:
type: string
description:
type: string
sectorNodes:
type: array
items:
$ref: '#/components/schemas/SectorNode'
26 changes: 26 additions & 0 deletions libs/converter-openapi/test/basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,4 +408,30 @@ describe('Basic', () => {
],
});
});

it('case 8 (self-referencing component schema)', async () => {
const oas = parse(loadAsset('basic-8.yaml'));
const config = await convert(oas);

expect(config).toEqual({
services: [
{
name: 'SectorAPI',
routes: [
{
methods: ['GET'],
name: 'getSectors',
uris: ['/v3/sectors'],
},
],
upstream: {
nodes: [{ host: 'localhost', port: 8080, weight: 100 }],
pass_host: 'pass',
scheme: 'http',
timeout: { connect: 60, read: 60, send: 60 },
},
},
],
});
});
});
Loading