import { OpenAPIV3 } from "openapi-types"; import { Collection, Description, Entity, Property } from "./types"; type APISchemas = { [key: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.ArraySchemaObject | OpenAPIV3.NonArraySchemaObject; }; function flatten<T>(acc: T[], val: T[]): T[] { return acc.concat(val); } function generateSchemas(entities: Entity[]): APISchemas { return fromEntites(entities.map(generateEntitySchemas).reduce(flatten, [])); } function generateEntitySchemas(entity: Entity): [string, OpenAPIV3.NonArraySchemaObject][] { const payloadSchema: OpenAPIV3.NonArraySchemaObject = { type: "object", required: entity.properties.filter(p => !p.isNullable).map(p => p.key).concat(["id", "ref"]), properties: fromEntites(addIdProperties(entity.properties.map(generateProperty))), }; const inputSchema: OpenAPIV3.NonArraySchemaObject = { type: "object", required: entity.properties.filter(p => !p.isNullable).map(p => p.key), properties: fromEntites(entity.properties.map(generateProperty)), }; return [ [entity.name, payloadSchema], [entity.name + "Input", inputSchema], ]; } function addIdProperties(properties: [string, OpenAPIV3.NonArraySchemaObject][]): [string, OpenAPIV3.NonArraySchemaObject][] { return [ ["id", { type: "number", format: "int32" }], ...properties, ["ref", { type: "string", format: "uri" }], ]; } function fromEntites<T>(list: [string, T][]): { [key: string]: T; } { const obj: { [key: string]: T; } = {}; for (const mapped of list) { obj[mapped[0]] = mapped[1]; } return obj; } function generateProperty(property: Property): [string, OpenAPIV3.NonArraySchemaObject] { const prop: OpenAPIV3.NonArraySchemaObject = { type: property.type, } return [ property.key, prop, ]; } function generatePaths(collections: Collection[]): OpenAPIV3.PathsObject { const paths = [ ...collections.map(generateCollectionPath), ...collections.map(generateEntityPath), ]; return fromEntites(paths); } export function collectionRoute(collection: Collection): string { return "/" + collection.name.toLowerCase(); } function generateCollectionPath(collection: Collection): [string, OpenAPIV3.PathItemObject] { const route = collectionRoute(collection); const path: OpenAPIV3.PathItemObject = { get: generateGetAllOperation(collection), post: generateCreateOperation(collection), }; return [route, path]; } function generateGetAllOperation(collection: Collection): OpenAPIV3.OperationObject { const operation: OpenAPIV3.OperationObject = { summary: "Returns all " + collection.name, tags: [collection.name], responses: { "200": { description: `Lists all ${collection.name}`, content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/" + collection.entities.name } } } } } } }; return operation; } function generateCreateOperation(collection: Collection): OpenAPIV3.OperationObject { const operation: OpenAPIV3.OperationObject = { summary: "Creates a new " + collection.entities.name, tags: [collection.name], requestBody: { required: true, description: "Data for the new " + collection.entities.name, content: { "application/json": { schema: { $ref: "#/components/schemas/" + collection.entities.name + "Input" } } } }, responses: { "201": { description: `Creates a new ${collection.entities.name}`, content: { "application/json": { schema: { $ref: "#/components/schemas/" + collection.entities.name } } } }, "400": { $ref: "#/components/responses/BadRequest", }, } } return operation; } function generateEntityPath(collection: Collection): [string, OpenAPIV3.PathItemObject] { const route = itemRoute(collection); const path: OpenAPIV3.PathItemObject = { get: generateGetSpecificOperation(collection), post: generatePostSpecificOperation(collection), delete: generateDeleteSpecific(collection), }; return [route, path]; } function itemRoute(collection: Collection): string { return collectionRoute(collection) + "/{" + idParameterName(collection.entities) + "}"; } function idParameterName(entity: Entity): string { return entity.name.toLowerCase() + "Id"; } function generateGetSpecificOperation(collection: Collection): OpenAPIV3.OperationObject { const entity: Entity = collection.entities; const operation: OpenAPIV3.OperationObject = { summary: "Returns a specific " + entity.name, tags: [collection.name], parameters: [entityPathParameter(entity)], responses: { "200": { description: "Returns one specific " + entity.name, content: { "application/json": { schema: { $ref: "#/components/schemas/" + entity.name, } } } }, "404": { $ref: "#/components/responses/NotFound", } } }; return operation; } function generatePostSpecificOperation(collection: Collection): OpenAPIV3.OperationObject { const entity: Entity = collection.entities; const operation: OpenAPIV3.OperationObject = { summary: "Replaces a specific " + entity.name, tags: [collection.name], parameters: [entityPathParameter(entity)], requestBody: { required: true, description: "Data to replace a specific " + collection.entities.name, content: { "application/json": { schema: { $ref: "#/components/schemas/" + collection.entities.name + "Input" } } } }, responses: { "200": { description: `Successfully updated the ${entity.name}`, content: { "application/json": { schema: { $ref: "#/components/schemas/" + entity.name, } } } }, "400": { $ref: "#/components/responses/BadRequest", }, "404": { $ref: "#/components/responses/NotFound", } } }; return operation; } function generateDeleteSpecific(collection: Collection): OpenAPIV3.OperationObject { const entity: Entity = collection.entities; const operation: OpenAPIV3.OperationObject = { summary: "Deletes a specific " + entity.name, tags: [collection.name], parameters: [entityPathParameter(entity)], responses: { "204": { description: `Successfully deleted the ${entity.name} or it never existed`, } } }; return operation; } function entityPathParameter(entity: Entity): OpenAPIV3.ParameterObject { return { in: "path", name: idParameterName(entity), required: true, schema: { type: "integer", }, }; } const NotFoundResponse: OpenAPIV3.ResponseObject = { description: "The specified resource does not exist.", content: { "text/plain": { schema: { type: "string", example: "not found", } } } }; const BadRequestResponse: OpenAPIV3.ResponseObject = { description: "The supplied data in invalid. See the response text for more information.", content: { "text/plain": { schema: { type: "string", example: "A required field is missing.", } } } } export function generateOpenAPI(description: Description): OpenAPIV3.Document { const doc: OpenAPIV3.Document = { openapi: "3.0.0", info: { title: "API for " + description.collections.map(c => c.name).join(", "), version: "1.0.0", description: "Generated by ServerGenerator at " + new Date().toISOString(), }, servers: [{ url: "http://localhost:8080" }], components: { schemas: generateSchemas(description.collections.map(c => c.entities)), responses: { NotFound: NotFoundResponse, BadRequest: BadRequestResponse } }, paths: generatePaths(description.collections), }; return doc; }