Add script to generate webhooks
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
// Based on https://github.com/github/docs/blob/9be5ebb4855be066ac22a3ae0dafb9d615c7dfb4/src/rest/scripts/utils/get-body-params.js
|
||||
|
||||
// If there is a oneOf at the top level, then we have to present just one
|
||||
// in the docs. We don't currently have a convention for showing more than one
|
||||
// set of input parameters in the docs. Having a top-level oneOf is also very
|
||||
// uncommon.
|
||||
// Currently there aren't very many operations that require this treatment.
|
||||
// As an example, the 'Add status check contexts' and 'Set status check contexts'
|
||||
// operations have a top-level oneOf.
|
||||
|
||||
async function getTopLevelOneOfProperty(schema: any) {
|
||||
if (!schema.oneOf) {
|
||||
throw new Error("Schema does not have a requestBody oneOf property defined");
|
||||
}
|
||||
if (!(Array.isArray(schema.oneOf) && schema.oneOf.length > 0)) {
|
||||
throw new Error("Schema requestBody oneOf property is not an array");
|
||||
}
|
||||
// When a oneOf exists but the `type` differs, the case has historically
|
||||
// been that the alternate option is an array, where the first option
|
||||
// is the array as a property of the object. We need to ensure that the
|
||||
// first option listed is the most comprehensive and preferred option.
|
||||
const firstOneOfObject = schema.oneOf[0];
|
||||
const allOneOfAreObjects = schema.oneOf.every((elem: any) => elem.type === "object");
|
||||
let required = firstOneOfObject.required || [];
|
||||
let properties = firstOneOfObject.properties || {};
|
||||
|
||||
// When all of the oneOf objects have the `type: object` we
|
||||
// need to display all of the parameters.
|
||||
// This merges all of the properties and required values.
|
||||
if (allOneOfAreObjects) {
|
||||
for (const each of schema.oneOf.slice(1)) {
|
||||
Object.assign(firstOneOfObject.properties, each.properties);
|
||||
required = firstOneOfObject.required.concat(each.required);
|
||||
}
|
||||
properties = firstOneOfObject.properties;
|
||||
}
|
||||
return {properties, required};
|
||||
}
|
||||
|
||||
// Gets the body parameters for a given schema recursively.
|
||||
export async function getBodyParams(schema: any, topLevel = false): Promise<any> {
|
||||
const bodyParametersParsed = [];
|
||||
const schemaObject = schema.oneOf && topLevel ? await getTopLevelOneOfProperty(schema) : schema;
|
||||
const properties: Record<string, any> = schemaObject.properties || {};
|
||||
const required = schemaObject.required || [];
|
||||
|
||||
// Most operation requestBody schemas are objects. When the type is an array,
|
||||
// there will not be properties on the `schema` object.
|
||||
if (topLevel && schema.type === "array") {
|
||||
const childParamsGroups = [];
|
||||
const arrayType = schema.items.type;
|
||||
const paramType = [schema.type];
|
||||
if (arrayType === "object") {
|
||||
childParamsGroups.push(...(await getBodyParams(schema.items, false)));
|
||||
} else {
|
||||
paramType.splice(paramType.indexOf("array"), 1, `array of ${arrayType}s`);
|
||||
}
|
||||
const paramDecorated = await getTransformedParam(schema, paramType, {
|
||||
required,
|
||||
topLevel,
|
||||
childParamsGroups
|
||||
});
|
||||
return [paramDecorated];
|
||||
}
|
||||
|
||||
for (const [paramKey, param] of Object.entries(properties)) {
|
||||
// OpenAPI 3.0 only had a single value for `type`. OpenAPI 3.1
|
||||
// will either be a single value or an array of values.
|
||||
// This makes type an array regardless of how many values the array
|
||||
// includes. This allows us to support 3.1 while remaining backwards
|
||||
// compatible with 3.0.
|
||||
const paramType = Array.isArray(param.type) ? param.type : [param.type];
|
||||
const additionalPropertiesType = param.additionalProperties
|
||||
? Array.isArray(param.additionalProperties.type)
|
||||
? param.additionalProperties.type
|
||||
: [param.additionalProperties.type]
|
||||
: [];
|
||||
const childParamsGroups = [];
|
||||
|
||||
// If the parameter is an array or object there may be child params
|
||||
// If the parameter has oneOf or additionalProperties, they need to be
|
||||
// recursively read too.
|
||||
|
||||
// There are a couple operations with additionalProperties, which allows
|
||||
// the api to define input parameters with the type dictionary. These are the only
|
||||
// two operations (at the time of adding this code) that use additionalProperties
|
||||
// Create a snapshot of dependencies for a repository
|
||||
// Update a gist
|
||||
if (param.additionalProperties && additionalPropertiesType.includes("object")) {
|
||||
const keyParam = {
|
||||
type: "object",
|
||||
name: "key",
|
||||
description: `A user-defined key to represent an item in \`${paramKey}\`.`,
|
||||
isRequired: param.required,
|
||||
enum: param.enum,
|
||||
default: param.default,
|
||||
childParamsGroups: [] as any[]
|
||||
};
|
||||
keyParam.childParamsGroups.push(...(await getBodyParams(param.additionalProperties, false)));
|
||||
childParamsGroups.push(keyParam);
|
||||
} else if (paramType && paramType.includes("array")) {
|
||||
const arrayType = param.items.type;
|
||||
if (arrayType) {
|
||||
paramType.splice(paramType.indexOf("array"), 1, `array of ${arrayType}s`);
|
||||
}
|
||||
if (arrayType === "object") {
|
||||
childParamsGroups.push(...(await getBodyParams(param.items, false)));
|
||||
}
|
||||
} else if (paramType && paramType.includes("object")) {
|
||||
childParamsGroups.push(...(await getBodyParams(param, false)));
|
||||
} else if (param && param.oneOf) {
|
||||
// get concatenated description and type
|
||||
const descriptions = [];
|
||||
for (const childParam of param.oneOf) {
|
||||
paramType.push(childParam.type);
|
||||
// If there is no parent description, create a description from
|
||||
// each type
|
||||
if (!param.description) {
|
||||
if (childParam.type === "array") {
|
||||
if (childParam.items.description) {
|
||||
descriptions.push({
|
||||
type: childParam.type,
|
||||
description: childParam.items.description
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (childParam.description) {
|
||||
descriptions.push({type: childParam.type, description: childParam.description});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Occasionally, there is no parent description and the description
|
||||
// is in the first child parameter.
|
||||
const oneOfDescriptions = descriptions.length ? descriptions[0].description : "";
|
||||
if (!param.description) param.description = oneOfDescriptions;
|
||||
|
||||
// This is a workaround for an operation that incorrectly defines allOf for a
|
||||
// body parameter. As a workaround, we will use the first object in the list of
|
||||
// the allOf array. Otherwise, fallback to the first item in the array.
|
||||
// This isn't ideal, and in the case of an actual allOf occurrence, we should
|
||||
// handle it differently by merging all of the properties. There is currently
|
||||
// only one occurrence for the operation id repos/update-information-about-pages-site
|
||||
// See Ecosystem API issue number #3332 for future plans to fix this in the OpenAPI
|
||||
} else if (param && param.anyOf && Object.keys(param).length === 1) {
|
||||
const firstObject = Object.values(param.anyOf).find((item: any) => item.type === "object") as {
|
||||
description: string;
|
||||
required: boolean;
|
||||
};
|
||||
if (firstObject) {
|
||||
paramType.push("object");
|
||||
param.description = firstObject.description;
|
||||
param.isRequired = firstObject.required;
|
||||
childParamsGroups.push(...(await getBodyParams(firstObject, false)));
|
||||
} else {
|
||||
paramType.push(param.anyOf[0].type);
|
||||
param.description = param.anyOf[0].description;
|
||||
param.isRequired = param.anyOf[0].required;
|
||||
}
|
||||
}
|
||||
|
||||
const paramDecorated = await getTransformedParam(param, paramType, {
|
||||
paramKey,
|
||||
required,
|
||||
childParamsGroups,
|
||||
topLevel
|
||||
});
|
||||
bodyParametersParsed.push(paramDecorated);
|
||||
}
|
||||
return bodyParametersParsed;
|
||||
}
|
||||
|
||||
type Param = {
|
||||
type: "string" | "string or null" | "number" | "boolean" | "array" | "object" | "object or null" | "integer or null";
|
||||
name: string;
|
||||
in: string;
|
||||
isRequired: boolean;
|
||||
description: string;
|
||||
childParamsGroups?: Param[];
|
||||
enum?: string[];
|
||||
default: any;
|
||||
};
|
||||
|
||||
async function getTransformedParam(param: any, paramType: any, props: any) {
|
||||
const {paramKey, required, childParamsGroups, topLevel} = props;
|
||||
const paramDecorated = {} as Param;
|
||||
|
||||
// Supports backwards compatibility for OpenAPI 3.0
|
||||
// In 3.1 a nullable type is part of the param.type array and
|
||||
// the property param.nullable does not exist.
|
||||
if (param.nullable) paramType.push("null");
|
||||
paramDecorated.type = paramType.filter(Boolean).join(" or ");
|
||||
paramDecorated.name = paramKey;
|
||||
if (topLevel) {
|
||||
paramDecorated.in = "body";
|
||||
}
|
||||
paramDecorated.description = param.description;
|
||||
if (required && required.includes(paramKey)) {
|
||||
paramDecorated.isRequired = true;
|
||||
}
|
||||
if (childParamsGroups && childParamsGroups.length > 0) {
|
||||
paramDecorated.childParamsGroups = childParamsGroups;
|
||||
}
|
||||
if (param.enum) {
|
||||
paramDecorated.enum = param.enum;
|
||||
}
|
||||
|
||||
// we also want to catch default values of `false` for booleans
|
||||
if (param.default !== undefined) {
|
||||
paramDecorated.default = param.default;
|
||||
}
|
||||
return paramDecorated;
|
||||
}
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
import fetch from "node-fetch";
|
||||
import {promises as fs} from "fs";
|
||||
import Webhook from "./webhook.js";
|
||||
|
||||
const SCHEMA_URL =
|
||||
"https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/dereferenced/api.github.com.deref.json";
|
||||
const OUTPUT_PATH = "src/webhooks.json";
|
||||
|
||||
const schema: any = await fetch(SCHEMA_URL).then(res => res.json());
|
||||
|
||||
const rawWebhooks = Object.values(schema["x-webhooks"]) as any[];
|
||||
if (!rawWebhooks) {
|
||||
throw new Error("No webhooks found in schema");
|
||||
}
|
||||
|
||||
const webhooks = [];
|
||||
for (const webhook of Object.values(rawWebhooks)) {
|
||||
webhooks.push(new Webhook(webhook.post));
|
||||
}
|
||||
|
||||
await Promise.all(webhooks.map(webhook => webhook.process()));
|
||||
|
||||
// The category is the name of the webhook
|
||||
const categorizedWebhooks: Record<string, Record<string, Webhook>> = {};
|
||||
for (const webhook of webhooks) {
|
||||
if (!webhook.action) webhook.action = "default";
|
||||
|
||||
if (categorizedWebhooks[webhook.category]) {
|
||||
categorizedWebhooks[webhook.category][webhook.action] = webhook;
|
||||
} else {
|
||||
categorizedWebhooks[webhook.category] = {};
|
||||
categorizedWebhooks[webhook.category][webhook.action] = webhook;
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeFile(OUTPUT_PATH, JSON.stringify(categorizedWebhooks, null, 2));
|
||||
@@ -0,0 +1,57 @@
|
||||
// Based on https://github.com/github/docs/blob/main/src/rest/scripts/utils/webhook.js
|
||||
|
||||
import {getBodyParams} from "./get-body-params.js";
|
||||
|
||||
const NO_CHILD_PROPERTIES = ["action", "enterprise", "installation", "organization", "repository", "sender"];
|
||||
|
||||
export default class Webhook {
|
||||
public description: string;
|
||||
public summary: string;
|
||||
public bodyParameters: any[];
|
||||
public availability: string[];
|
||||
public action: string;
|
||||
public category: string;
|
||||
#webhook: any;
|
||||
|
||||
constructor(webhook: any) {
|
||||
this.#webhook = webhook;
|
||||
this.description = webhook.description;
|
||||
this.summary = webhook.summary;
|
||||
this.bodyParameters = [];
|
||||
this.availability = webhook["x-github"]["supported-webhook-types"];
|
||||
|
||||
// for some webhook action types (like some pull-request webhook types) the
|
||||
// schema properties are under a oneOf so we try and take the action from
|
||||
// the first one (the action will be the same across oneOf items)
|
||||
this.action =
|
||||
webhook.requestBody.content["application/json"]?.schema?.properties?.action?.enum?.[0] ||
|
||||
webhook.requestBody.content["application/json"]?.schema?.oneOf?.[0]?.properties?.action?.enum?.[0] ||
|
||||
null;
|
||||
|
||||
// The OpenAPI uses hyphens for the webhook names, but the webhooks
|
||||
// are sent using underscores (e.g. `branch_protection_rule` instead
|
||||
// of `branch-protection-rule`)
|
||||
this.category = webhook["x-github"].subcategory.replaceAll("-", "_");
|
||||
return this;
|
||||
}
|
||||
|
||||
public async process() {
|
||||
await this.renderBodyParameterDescriptions();
|
||||
}
|
||||
|
||||
private async renderBodyParameterDescriptions() {
|
||||
if (!this.#webhook.requestBody) return [];
|
||||
const schema = this.#webhook.requestBody.content["application/json"]?.schema || {};
|
||||
|
||||
const isObject = schema !== null && typeof schema === "object";
|
||||
|
||||
this.bodyParameters = isObject ? await getBodyParams(schema, true) : [];
|
||||
|
||||
// Removes the children of the common properties
|
||||
this.bodyParameters.forEach(param => {
|
||||
if (NO_CHILD_PROPERTIES.includes(param.name)) {
|
||||
param.childParamsGroups = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user