Skip to content

Commit

Permalink
feat: add new workflows
Browse files Browse the repository at this point in the history
  • Loading branch information
smizell committed Mar 15, 2022
1 parent c5e5c0d commit 47ae4a3
Show file tree
Hide file tree
Showing 14 changed files with 829 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@babel/runtime": "^7.17.2",
"@useoptic/api-checks": "0.22.15",
"@useoptic/json-pointer-helpers": "0.22.15",
"@useoptic/openapi-cli": "^0.23.0",
"@useoptic/openapi-io": "0.22.15",
"@useoptic/openapi-utilities": "0.22.15",
"chai": "^4.3.4",
Expand Down
12 changes: 12 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

import { makeCiCli } from "@useoptic/api-checks/build/ci-cli/make-cli";
import { newSnykApiCheckService } from "./service";
import { Command } from "commander";
import {
addOperationCommand,
createResourceCommand,
} from "./workflows/commands";

const apiCheckService = newSnykApiCheckService();
const cli = makeCiCli("sweater-comb", apiCheckService, {
Expand All @@ -13,4 +18,11 @@ const cli = makeCiCli("sweater-comb", apiCheckService, {
ciProvider: "circleci",
});

const workflowCommand = new Command("workflow").description(
"workflows for designing and building APIs",
);
workflowCommand.addCommand(createResourceCommand());
workflowCommand.addCommand(addOperationCommand());
cli.addCommand(workflowCommand);

cli.parse(process.argv);
125 changes: 125 additions & 0 deletions src/workflows/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import fs from "fs";
import path from "path";
import { writeYaml } from "@useoptic/openapi-io";
import { applyTemplate } from "@useoptic/openapi-cli";
import { addCreateOperation } from "./templates/operations/create";
import { addListOperation } from "./templates/operations/list";
import { addGetOperation } from "./templates/operations/get";
import { addUpdateOperation } from "./templates/operations/update";
import { addDeleteOperation } from "./templates/operations/delete";
import { buildNewResourceSpec } from "./templates/new-resource-spec";

export async function createResource(resourceName, pluralResourceName) {
// TODO: the SDK should probably help with the generation of new files
// and allow ergonomic use of a SpecTemplate to do so
const titleResourceName = titleCase(resourceName);
const version = getResourceVersion();
const collectionPath = `/${pluralResourceName}`;
if (!fs.existsSync(path.join(".", "resources")))
throw new Error(
"Resource directory does not exist. Are you sure you're in the right directory?",
);
await fs.mkdirSync(path.join(".", "resources", pluralResourceName, version), {
recursive: true,
});
const spec = buildNewResourceSpec(titleResourceName);
const specYaml = writeYaml(spec);
fs.writeFileSync(
path.join(".", "resources", pluralResourceName, version, "spec.yaml"),
specYaml,
);
}

export async function addOperation(
specFilePath, // TODO: consider how workflows can provided with more sophisticated context
operation,
resourceName,
pluralResourceName,
) {
const titleResourceName = titleCase(resourceName);
const collectionPath = `/${pluralResourceName}`;
const itemPath = `${collectionPath}/{${resourceName}_id}`;
switch (operation) {
case "all":
// TODO: consider how this impacts performance (round trip to the FS for each call)
// and whether that's something we need to address here
await applyTemplate(addCreateOperation, specFilePath, {
collectionPath,
resourceName,
titleResourceName,
});
await applyTemplate(addListOperation, specFilePath, {
collectionPath,
resourceName,
titleResourceName,
});
await applyTemplate(addGetOperation, specFilePath, {
itemPath,
resourceName,
titleResourceName,
});
await applyTemplate(addUpdateOperation, specFilePath, {
itemPath,
resourceName,
titleResourceName,
});
await applyTemplate(addDeleteOperation, specFilePath, {
itemPath,
resourceName,
titleResourceName,
});
break;
case "create":
await applyTemplate(addCreateOperation, specFilePath, {
collectionPath,
resourceName,
titleResourceName,
});
break;
case "get-many":
await applyTemplate(addListOperation, specFilePath, {
collectionPath,
resourceName,
titleResourceName,
});
break;
case "get-one":
await applyTemplate(addGetOperation, specFilePath, {
itemPath,
resourceName,
titleResourceName,
});
break;
case "update":
await applyTemplate(addUpdateOperation, specFilePath, {
itemPath,
resourceName,
titleResourceName,
});
break;
case "delete":
await applyTemplate(addDeleteOperation, specFilePath, {
itemPath,
resourceName,
titleResourceName,
});
break;
}
}

//-----

function getResourceVersion(): string {
const today = new Date();
return `${today.getFullYear()}-${padWithZero(today.getMonth())}-${padWithZero(
today.getUTCDay(),
)}`;
}

function padWithZero(value: number): string {
return ("00" + value).slice(-2);
}

function titleCase(value: string): string {
return value[0].toUpperCase() + value.slice(1);
}
41 changes: 41 additions & 0 deletions src/workflows/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Command, Argument } from "commander";
import { addOperation, createResource } from "./actions";

export function createResourceCommand() {
const command = new Command("create-resource")
.addArgument(new Argument("<resource-name>", "[resource-name]"))
.addArgument(
new Argument("<plural-resource-name>", "[plural-resource-name]"),
)
.action(async (resourceName, pluralResourceName) => {
return createResource(resourceName, pluralResourceName);
});

command.description("create a new resource");

return command;
}

export function addOperationCommand() {
const command = new Command("add-operation")
.addArgument(new Argument("<openapi>", "path to openapi file"))
.addArgument(new Argument("<operation>", "[operation]"))
.addArgument(new Argument("<resource-name>", "[resource-name]"))
.addArgument(
new Argument("<plural-resource-name>", "[plural-resource-name]"),
)
.action(
async (specFilePath, operation, resourceName, pluralResourceName) => {
return addOperation(
specFilePath,
operation,
resourceName,
pluralResourceName,
);
},
);

command.description("add an operation to an existing resource");

return command;
}
81 changes: 81 additions & 0 deletions src/workflows/templates/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
export const refs = {
restCommon: asRef(
"https://raw.githubusercontent.com/snyk/sweater-comb/v1.6.0/components/common.yaml",
),
headers: {
versionRequested: header("VersionRequestedResponseHeader"),
versionServed: header("VersionServedResponseHeader"),
requestId: header("RequestIdResponseHeader"),
versionStage: header("VersionStageResponseHeader"),
deprecation: header("DeprecationHeader"),
sunset: header("SunsetHeader"),
},
responses: {
"204": response("204"),
"400": response("400"),
"401": response("401"),
"403": response("403"),
"404": response("404"),
"409": response("409"),
"500": response("500"),
},
parameters: {
version: parameter("Version"),
startingAfter: parameter("StartingAfter"),
endingBefore: parameter("EndingBefore"),
limit: parameter("limit"),
},
schemas: {
paginationLinks: schema("PaginatedLinks"),
jsonApi: schema("JsonApi"),
types: schema("Types"),
selfLink: schema("SelfLink"),
},
};

export const paginationParameters = [
refs.parameters.startingAfter,
refs.parameters.endingBefore,
refs.parameters.limit,
];

export const commonHeaders = {
"snyk-version-requested": refs.headers.versionRequested,
"snyk-version-served": refs.headers.versionServed,
"snyk-request-id": refs.headers.requestId,
"snyk-version-lifecycle-stage": refs.headers.versionStage,
deprecation: refs.headers.deprecation,
sunset: refs.headers.sunset,
};

export const { "204": _, ...commonResponses } = refs.responses;

function header(name: string) {
return {
$ref: `#/components/x-rest-common/headers/${name}`,
};
}

function response(name: string) {
return {
$ref: `#/components/x-rest-common/responses/${name}`,
};
}

function parameter(name: string) {
return {
$ref: `#/components/x-rest-common/parameters/${name}`,
};
}

function schema(name: string) {
return {
$ref: `#/components/x-rest-common/schemas/${name}`,
};
}

function asRef(ref: string) {
return {
$ref: ref,
};
}
38 changes: 38 additions & 0 deletions src/workflows/templates/new-resource-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { OpenAPIV3 } from "@useoptic/openapi-utilities";
import { refs } from "./common";

export function buildNewResourceSpec(
titleResourceName: string,
): OpenAPIV3.Document {
const spec: OpenAPIV3.Document = baseOpenApiSpec(titleResourceName);
if (!spec.components) spec.components = {};
if (!spec.components.schemas) spec.components.schemas = {};
spec.components.schemas[`${titleResourceName}Attributes`] = {
type: "object",
properties: {},
};
// @ts-ignore
// Ignoring since `x-rest-common` is not correct according to the types.
spec.components["x-rest-common"] = refs.restCommon;
return spec;
}

function baseOpenApiSpec(titleResourceName: string): OpenAPIV3.Document {
return {
openapi: "3.0.3",
info: {
title: `${titleResourceName} Resource`,
version: "3.0.0",
},
servers: [
{ url: "https://api.snyk.io/v3", description: "Public Snyk API" },
],
tags: [
{
name: titleResourceName,
description: `Short description of what ${titleResourceName} represents`,
},
],
paths: {},
};
}
74 changes: 74 additions & 0 deletions src/workflows/templates/operations/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { commonHeaders, commonResponses, refs } from '../common';
import { OpenAPIV3 } from 'openapi-types';
import {
buildCreateRequestSchema,
buildItemResponseSchema,
ensureRelationSchema,
} from '../schemas';
import { SpecTemplate } from '@useoptic/openapi-cli';

export const addCreateOperation = SpecTemplate.create(
'add-create-operation',
function addCreateOperation(
spec: OpenAPIV3.Document,
options: {
collectionPath: string;
resourceName: string;
titleResourceName: string;
}
): void {
const { collectionPath, resourceName, titleResourceName } = options;
if (!spec.paths) spec.paths = {};
if (!spec.paths[collectionPath]) spec.paths[collectionPath] = {};
if (!spec.components) spec.components = {};
if (!spec.components.schemas) spec.components.schemas = {};
spec.paths[collectionPath]!.post = buildCreateOperation(
resourceName,
titleResourceName
);
const attributes =
spec.components?.schemas?.[`${titleResourceName}Attributes`];
if (!attributes)
throw new Error(`Could not find ${titleResourceName}Attributes schema`);
spec.components.schemas[`${titleResourceName}CreateAttributes`] =
attributes;
ensureRelationSchema(spec, titleResourceName);
}
);

function buildCreateOperation(
resourceName: string,
titleResourceName: string
): OpenAPIV3.OperationObject {
const itemResponseSchema = buildItemResponseSchema(
resourceName,
titleResourceName
);
const createRequestSchema = buildCreateRequestSchema(titleResourceName);
return {
summary: `Create a new ${resourceName}`,
description: `Create a new ${resourceName}`,
operationId: `create${titleResourceName}`,
tags: [titleResourceName],
parameters: [refs.parameters.version],
requestBody: {
content: {
'application/json': {
schema: createRequestSchema,
},
},
},
responses: {
'201': {
description: `Created ${resourceName} successfully`,
headers: commonHeaders,
content: {
'application/vnd.api+json': {
schema: itemResponseSchema,
},
},
},
...commonResponses,
},
};
}
Loading

0 comments on commit 47ae4a3

Please sign in to comment.