Newer
Older
ServerGenerator / src / OpenApiGenerator.ts
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;
}