-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
829 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: {}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}; | ||
} |
Oops, something went wrong.