Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add customization options for the OpenAPI reference #126

Merged
merged 5 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ To build this project locally, you will need to have [Node.js](https://nodejs.or
You can use the following commands to build this project locally:

- `npm install` to install dependencies.
- `npm run generate` to build the OpenAPI reference.
- `npm start` to start a development server.
- `npm run build` to build a production release (this may be necessary to work with multi-lingual content).
- `npm run serve` to serve a production release locally.
Expand All @@ -33,6 +34,18 @@ $ npm run docusaurus write-translations
$ npm run docusaurus write-translations -- -l de
```

### Provide a custom description for an OpenAPI operation

By default, the OpenAPI description pages use the OpenAPI `.description` field of the respective resource. You can override the description with a custom markdown document by placing it in `generator/overlays/<apiVersion>/<operation-id>/description.md`. Please note that you will need to run `npm run generate` for changes to take effect.

### Override an OpenAPI tag index page.

By default, the tag index pages will be generated from the tag description and an auto-generated list of all operations. You can override this index page by providing a custom markdown document at `generator/overlays/<apiVersion>/<tag-name>.mdx`. Please note that you will need to run `npm run generate` for changes to take effect.

### Provide custom usage examples for OpenAPI operations

The usage examples for OpenAPI operations are generated from the OpenAPI specification. You can override the usage examples by providing a custom markdown document at `generator/overlays/<apiVersion>/<operation-id>/example-<language>.md`. Please note that you will need to run `npm run generate` for changes to take effect. Supported languages are `curl`, `javascript`, `php` and `cli`.

[md]: https://www.markdownguide.org
[mdx]: https://mdxjs.com
[docu-md]: https://docusaurus.io/docs/markdown-features
Expand Down
73 changes: 58 additions & 15 deletions generator/generate-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,67 @@ function determineServerURLAndBasePath(apiVersion: APIVersion, spec: OpenAPIV3.D
return [serverURL, basePath];
}

function renderAPISpecToFile(operationFile: string, frontMatterYAML: string, urlPathWithBase: string, method: string, serializedSpec: string, serverURL: string, apiVersion: APIVersion) {
function loadDescriptionOverride(apiVersion: APIVersion, operationId: string): string | undefined {
const descriptionOverridePath = path.join("generator", "overlays", apiVersion, operationId, "description.md");
if (fs.existsSync(descriptionOverridePath)) {
return fs.readFileSync(descriptionOverridePath, { encoding: "utf-8" });
}
return undefined;
}

function loadCodeExample(apiVersion: APIVersion, operationId: string, language: string): string | undefined {
const codeExamplePath = path.join("generator", "overlays", apiVersion, operationId, `example-${language}.md`);
if (fs.existsSync(codeExamplePath)) {
return fs.readFileSync(codeExamplePath, { encoding: "utf-8" });
}
return undefined;
}

function renderAPISpecToFile(
operationFile: string,
urlPathWithBase: string,
method: string,
spec: OpenAPIV3.OperationObject,
serverURL: string,
apiVersion: APIVersion,
) {
const withSDKExamples = apiVersion !== "v1";
const serializedSpec = JSON.stringify(spec);
const summary: string = stripTrailingDot(spec.summary);

const frontMatter = yaml.stringify({
title: summary ?? spec.operationId,
description: spec.description ?? "",
openapi: {
method
}
});

const descriptionOverride = loadDescriptionOverride(apiVersion, spec.operationId);
const exampleOverrides = [
["curl", "cURL", loadCodeExample(apiVersion, spec.operationId, "curl")],
["javascript", "JavaScript SDK", loadCodeExample(apiVersion, spec.operationId, "javascript")],
["php", "PHP SDK", loadCodeExample(apiVersion, spec.operationId, "php")],
["cli", "mw CLI", loadCodeExample(apiVersion, spec.operationId, "cli")],
].filter(([,, i]) => i !== undefined).map(([key, label, content]) => `<TabItem key="${key}" value="${key}" label="${label}">\n\n${content}\n\n</TabItem>`);

// Yes, this is JavaScript that renders more JavaScript (or mdx, to be precise).
// Yes, this is a bit weird and opens up a whole can of worms. Oh, well.

// language=text
fs.writeFileSync(operationFile, `---
${frontMatterYAML}
${frontMatter}
---

import {OperationRequest, OperationResponses} from "@site/src/components/openapi/OperationReference";
import {OperationMetadata} from "@site/src/components/openapi/OperationMetadata";
import {OperationUsage} from "@site/src/components/openapi/OperationUsage";
import OperationLink from "@site/src/components/OperationLink";
import TabItem from "@theme/TabItem";

<OperationMetadata path="${urlPathWithBase}" method="${method}" spec={${serializedSpec}} />
<OperationMetadata path="${urlPathWithBase}" method="${method}" spec={${serializedSpec}} withDescription={${descriptionOverride === undefined}} />

${descriptionOverride ?? ""}

## Request

Expand All @@ -59,7 +104,9 @@ import {OperationUsage} from "@site/src/components/openapi/OperationUsage";

## Usage examples

<OperationUsage method="${method}" url="${urlPathWithBase}" spec={${serializedSpec}} baseURL="${serverURL}" withJavascript={${withSDKExamples}} withPHP={${withSDKExamples}} />
<OperationUsage method="${method}" url="${urlPathWithBase}" spec={${serializedSpec}} baseURL="${serverURL}" withJavascript={${withSDKExamples}} withPHP={${withSDKExamples}}>
${exampleOverrides.join("\n\n")}
</OperationUsage>

`);
}
Expand All @@ -79,6 +126,12 @@ function stripTrailingDot(str: string | undefined): string | undefined {
async function renderTagIndexPage(apiVersion: APIVersion, name: string, description: string, outputPath: string, sidebarItems: any[]): Promise<void> {
const indexFile = path.join(outputPath, "index.mdx");

const overrideFile = path.join("generator", "overlays", apiVersion, slugFromTagName(name) + ".mdx");
if (fs.existsSync(overrideFile)) {
fs.copyFileSync(overrideFile, indexFile);
return;
}

const frontMatter = yaml.stringify({
title: name,
description,
Expand Down Expand Up @@ -145,17 +198,7 @@ async function renderAPIDocs(apiVersion: APIVersion, outputPath: string) {
}
});

const frontMatter = {
title: summary ?? operation.operationId,
description: operation.description ?? "",
openapi: {
method
}
};

const frontMatterYAML = yaml.stringify(frontMatter);

renderAPISpecToFile(operationFile, frontMatterYAML, urlPathWithBase, method, serializedSpec, serverURL, apiVersion);
renderAPISpecToFile(operationFile, urlPathWithBase, method, operation, serverURL, apiVersion);
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions generator/overlays/v2/app-request-appinstallation/description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
This API operation requests a new app installation.

## Usage notes

### Determining the `appVersionId`

Both the app that should be installed and the specific version are identified by the `appVersionId` body parameter.

To determine available apps, use the <OperationLink tag="App" operation="app-list-apps" /> API operation. To determine available versions for a given app, use the app ID (as returned by the `app-list-apps` operation) as input parameter for the <OperationLink tag="App" operation="app-list-appversions" /> operation.

### On user inputs

Pay attention to the `userInputs` parameter in the request body. This parameter is a list of objects, each with a `name` and `value` property.

The allowed values for `name` are dependent on the app version being installed. To determine the required inputs for a given app version, inspect the `userInputs` property of the app version object returned by the <OperationLink tag="App" operation="app-list-appversions" /> operation.

### Determining when the installation is complete

Note that this operation does not block until the installation is complete. To determine the status of the installation, use the <OperationLink tag="App" operation="app-get-appinstallation" /> operation. When the operation is complete, the `.appVersion.current` property will be equal to `.appVersion.desired`.
14 changes: 14 additions & 0 deletions generator/overlays/v2/app-request-appinstallation/example-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
```shellsession
$ mw app create php \
--project-id $PROJECT_ID \
--site-title "My TYPO3 site"

$ mw app install typo3 \
--version 12.4.16 \
--install-mode composer \
--project-id $PROJECT_ID \
--admin-email [email protected] \
--admin-pass securepassword \
--admin-user admin \
--site-title "My TYPO3 site"
```
20 changes: 20 additions & 0 deletions generator/overlays/v2/customer.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: Organizations/Customers
description: The customer API allows you to manage your own organizations and users.
displayed_sidebar: apiSidebar

---

import OperationDocCardList from "@site/src/components/openapi/OperationDocCardList";

# Organizations/Customers

The customer API allows you to manage your own organizations and users.

:::important Important to know: "customers" vs. "organizations"

Please note the naming discrepancy between the API and the UI; the UI refers to **"organizations"**, which are referred to as **"customers"** in the API. These are **not** two different kinds of resources; they are the same thing, just differently named.

:::

<OperationDocCardList apiVersion="v2" tag="Customer" />
4 changes: 2 additions & 2 deletions i18n/en/docusaurus-plugin-content-docs/current.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,11 @@
"description": "The generated-index page description for category Conversation in sidebar apiSidebar"
},
"sidebar.apiSidebar.category.Customer": {
"message": "Customer",
"message": "Organization/Customer",
"description": "The label for category Customer in sidebar apiSidebar"
},
"sidebar.apiSidebar.category.Customer.link.generated-index.title": {
"message": "Customer",
"message": "Organization/Customer",
"description": "The generated-index page title for category Customer in sidebar apiSidebar"
},
"sidebar.apiSidebar.category.Customer.link.generated-index.description": {
Expand Down
2 changes: 2 additions & 0 deletions src/components/OperationLink/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export default function OperationLink({
operation,
children,
}: PropsWithChildren<OperationLinkProps>) {
children = children || <code>{operation}</code>;

const url = `/docs/v2/reference/${tag.toLowerCase()}/${operation}`;
return <Link to={url}>{children}</Link>;
}
4 changes: 3 additions & 1 deletion src/components/openapi/OperationMetadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@ export function OperationMetadata({
method,
path,
spec,
withDescription = true,
}: {
path: string;
method: string;
spec: OpenAPIV3.OperationObject;
withDescription?: boolean;
}) {
return (
<>
Expand Down Expand Up @@ -92,7 +94,7 @@ export function OperationMetadata({
</LabeledValue>
</ColumnLayout>
<hr />
{spec.description ? <Markdown>{spec.description}</Markdown> : null}
{spec.description && withDescription ? <Markdown>{spec.description}</Markdown> : null}
</>
);
}
42 changes: 25 additions & 17 deletions src/components/openapi/OperationUsage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface OperationUsageProps {
baseURL: string;
withJavascript?: boolean;
withPHP?: boolean;
children?: ReactElement<TabItemProps>[] | ReactElement<TabItemProps>;
}

export function OperationUsage(props: OperationUsageProps) {
Expand All @@ -25,38 +26,45 @@ export function OperationUsage(props: OperationUsageProps) {
withJavascript = true,
withPHP = true,
} = props;
let { children = [] } = props;

const items: ReactElement<TabItemProps>[] = [
<TabItem key="curl" value="curl" label="cURL">
<CodeBlock language="shell-session">
{generateCurlCodeExample(method, url, spec, baseURL)}
</CodeBlock>
</TabItem>,
];
if (!Array.isArray(children)) {
children = [children];
}

if (withJavascript) {
items.push(
if (withPHP && !children.some(i => i.key === "php")) {
children.unshift(
<TabItem key="php" value="php" label="PHP SDK">
<CodeBlock language="php">
{generatePHPCodeExample(method, url, spec, baseURL)}
</CodeBlock>
</TabItem>
);
}

if (withJavascript && !children.some(i => i.key === "javascript")) {
children.unshift(
<TabItem key="javascript" value="javascript" label="JavaScript SDK">
<CodeBlock language="javascript">
{generateJavascriptCodeExample(method, url, spec, baseURL)}
</CodeBlock>
</TabItem>,
</TabItem>
);
}

if (withPHP) {
items.push(
<TabItem key="php" value="php" label="PHP SDK">
<CodeBlock language="php">
{generatePHPCodeExample(method, url, spec, baseURL)}
if (!children.some(i => i.key === "curl")) {
children.unshift(
<TabItem key="curl" value="curl" label="cURL">
<CodeBlock language="shell-session">
{generateCurlCodeExample(method, url, spec, baseURL)}
</CodeBlock>
</TabItem>,
</TabItem>
);
}

return (
<Tabs groupId="usage-language" defaultValue="curl">
{items}
{children}
</Tabs>
);
}