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

feat: add support for rendering open-api specification exports within vuepress #109

Merged
merged 41 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
63ee245
feat(open-api): add open-api typescript types
FreekVR Apr 4, 2024
09053b8
feat(open-api): add display of base open-api info and component struc…
FreekVR Apr 4, 2024
ecc3814
feat(open-api): add display of (example) responses and base path info…
FreekVR Apr 4, 2024
07481fa
feat(open-api): improve display of open-api operation details
FreekVR Apr 4, 2024
f73a61f
feat(open-api): display API responses using existing components
FreekVR Apr 4, 2024
9954f01
docs(open-api): add a test document for open-api output
FreekVR Apr 4, 2024
c2a7dd1
fix(open-api): fix dark mode not working correctly for the operations…
FreekVR Apr 5, 2024
7222522
docs(open-api): add json sample for MyParcel webhooks
FreekVR Apr 5, 2024
470a59e
fix(open-api): fix security / auth showing when it was an empty array
FreekVR Apr 5, 2024
9b04e08
feat(open-api): add basic $ref resolution support for openapi documents
FreekVR Apr 5, 2024
87bf5d6
feat(open-api): add basic display of open-api schemas
FreekVR Apr 5, 2024
7f7ed9b
feat(open-api): improve rendering of (response) schemas
FreekVR Apr 8, 2024
57b161f
feat(open-api): add display of request parameter and body information
FreekVR Apr 9, 2024
f00469c
feat(open-api): add consistent rendering of examples in request- and …
FreekVR Apr 10, 2024
f200a81
feat(open-api): add support for rendering request body description an…
FreekVR Apr 10, 2024
996d59d
feat(open-api): wrap reponse examples and schemas in details/expand b…
FreekVR Apr 10, 2024
3ede3d0
refactor(open-api): resolve type safety issues and create util file f…
FreekVR Apr 10, 2024
eb5141a
fix(open-api): fix tables not showing correctly in dark mode
FreekVR Apr 10, 2024
b31b8fd
feat(open-api): add a plugin to render openapi schema files as markdo…
FreekVR Apr 11, 2024
bf7ea0b
refactor(open-api): load yaml files from (remote) url rather than jso…
FreekVR Apr 12, 2024
b799fc0
feat(open-api): add and improve display of various schema requirements
FreekVR Apr 12, 2024
d7640b5
fix(open-api): add missing global components to the indexed list
FreekVR Apr 12, 2024
e20531f
feat(open-api): add top-level security information
FreekVR Apr 12, 2024
4697710
fix(open-api): fix example rendering "value" key if summary or descri…
FreekVR Apr 15, 2024
0cb2035
fix(open-api): don't show "Authorization" heading without any securit…
FreekVR Apr 15, 2024
a241ae7
feat(open-api): add auth/security requirements for individual operations
FreekVR Apr 15, 2024
6b1e0be
feat(open-api): support display of webhooks and make paths optional
FreekVR Apr 15, 2024
5d50d1f
ci(open-api): add nightly (re)deploy to update with changes from remo…
FreekVR Apr 15, 2024
92ec5d9
feat(open-api): add servers display if set
FreekVR Apr 16, 2024
7cd4a3c
fix(open-api): render all description fields as markdown
FreekVR Apr 16, 2024
ddcf197
docs(open-api): remove all unpublishable schemas
FreekVR Apr 16, 2024
7c7dfdc
style(open-api): clean up formatting
FreekVR Apr 19, 2024
a126501
refactor(open-api): add type definition to method prop for `OpenApiOp…
FreekVR Apr 19, 2024
2b71469
refactor(open-api): don't use indexes for keys
FreekVR Apr 19, 2024
0c44e96
refactor(open-api): process feedback
FreekVR Apr 19, 2024
b94573f
refactor(open-api): clean up and improve html formatting
FreekVR Apr 19, 2024
ec35d95
Update src/.vuepress/theme/client/components/global/OpenApiSchemaInfo…
FreekVR Apr 23, 2024
17ea752
refactor(open-api): replace array index with value index
FreekVR Apr 23, 2024
9d9d8b1
fix(open-api): fix security schemes never rendering
FreekVR Apr 23, 2024
02680c6
docs(open-api): do not publish address API for now
FreekVR Apr 23, 2024
ce11a5e
refactor(open-api): remove redundant checks for schema prop
FreekVR Apr 24, 2024
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
34 changes: 34 additions & 0 deletions .github/workflows/nightly-rebuild.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: 'Nightly rebuild 🌙'

on:
schedule:
- cron: '0 0 * * *'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
upload:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3

- uses: ./.github/actions/build

- uses: actions/upload-pages-artifact@v1
with:
path: src/.vuepress/dist

deploy-ghp:
needs: upload
runs-on: ubuntu-22.04
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v1
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"markdown-it-multimd-table": "^4.1.3",
"mock-fs": "^5.1.2",
"only-allow": "^1.1.1",
"openapi-types": "^12.1.3",
"plop": "^4.0.0",
"postcss-import": "^15.1.0",
"prettier": "^2.8.8",
Expand All @@ -90,7 +91,8 @@
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"vite-svg-loader": "^4.0.0",
"vitest": "^0.34.6"
"vitest": "^0.34.6",
"yaml": "^2.4.1"
},
"packageManager": "[email protected]",
"volta": {
Expand Down
5 changes: 5 additions & 0 deletions src/.vuepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {viteConfig} from './viteConfig';
import {myParcelTheme} from './theme';
import {sitemapPlugin} from './plugins/sitemap';
import {parseTranslationsPlugin} from './plugins/parseTranslations';
import {openApiPlugin} from './plugins/openApi';
import {googleTagManagerPlugin} from './plugins/gtm/node';
import {DIR_CONFIG, DIR_VUEPRESS} from './dirs';
import {head} from './config/head';
Expand All @@ -25,6 +26,10 @@ export default defineUserConfig({
}),

plugins: [
openApiPlugin({
yamlUrls: [],
}),

parseTranslationsPlugin({
defaultLocale: 'en-GB',
configDir: path.resolve(DIR_CONFIG, 'navigation'),
Expand Down
73 changes: 73 additions & 0 deletions src/.vuepress/plugins/openApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {parse as parseYaml} from 'yaml';
import {createPage, type Plugin} from 'vuepress';
import {type OpenAPIV3_1 as OpenApiType} from 'openapi-types';
import {kebabCase} from 'lodash-es';
import {resolveRefs} from '../theme/client/utils/openApiHelpers';

interface OpenApiPluginConfig {
yamlUrls: string[];
}

export const openApiPlugin = (config: OpenApiPluginConfig): Plugin => ({
name: '@myparcel/vuepress-openapi',
async onInitialized(app) {
// Find all yaml files in config.yamlUrls and create a page for each of them
await Promise.all(
config.yamlUrls?.map(async (url) => {
const document = (await fetch(url).then(async (res) => parseYaml(await res.text()))) as OpenApiType.Document;
// Use the basename of the file as the slug
const resolvedDocument = resolveRefs(document);

// Now generate the page contents
app.pages.push(await createPage(app, generateOpenApiPage(resolvedDocument)));
}),
);
},
});

function generateOpenApiPage(document: OpenApiType.Document) {
return {
path: `/api-reference/${kebabCase(document.info.title)}`,
content: `\
---
title: ${document.info.title}
description: ${document.info.description}
---
Version ${document.info.version}

${document.info.description ?? ''}

${document.security ? '## Authorization' : ''}
<OpenApiSecurityRequirements
:security='${JSON.stringify(document.security ?? [])}'
:security-schemes='${JSON.stringify(document.components?.securitySchemes ?? [])}' />

${document.servers?.length ? '## Servers' : ''}
<OpenApiServers :servers='${JSON.stringify(document.servers ?? [])}' />

${renderPaths(document, document.paths, 'Endpoints')}

${renderPaths(document, document.webhooks, 'Webhooks')}
`,
FreekVR marked this conversation as resolved.
Show resolved Hide resolved
};
}
FreekVR marked this conversation as resolved.
Show resolved Hide resolved

function renderPaths(
document: OpenApiType.Document,
paths: OpenApiType.PathsObject | undefined,
heading: string,
): string {
if (!paths || !Object.keys(paths).length) return '';

let chapters = `## ${heading}`;

for (const [path, pathObj] of Object.entries(paths)) {
chapters += `
### ${path}
\n
<OpenApiPath :path='${JSON.stringify(pathObj)}' :components='${JSON.stringify(document.components)}' title='${path}' />
`;
}

return chapters;
}
67 changes: 67 additions & 0 deletions src/.vuepress/theme/client/components/global/OpenApi.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<template>
<section class="open-api">
<OpenApiInfo :info="resolvedDocument.info" />

<article
v-for="(item, path) in resolvedDocument.paths"
:key="path">
FreekVR marked this conversation as resolved.
Show resolved Hide resolved
<OpenApiPath
v-if="item"
:path="item"
:components="document.components"
:title="path" />
</article>
</section>
</template>

<script setup lang="ts">
import {computed} from 'vue';
import {type OpenAPIV3_1 as OpenApiType} from 'openapi-types';
import {get} from 'lodash-es';
import OpenApiPath from './OpenApiPath.vue';
import OpenApiInfo from './OpenApiInfo.vue';

const props = defineProps<{
document: OpenApiType.Document;
}>();

const resolvedDocument = computed(() => resolveRefs(props.document));

// Recursively loop through the entire document and replace any $ref keys with their corresponding values
type RecursiveType = {
[key: string]: unknown | RecursiveType | RecursiveType[];
};

function resolveRefs(document: OpenApiType.Document): OpenApiType.Document {
FreekVR marked this conversation as resolved.
Show resolved Hide resolved
const resolvedDocument = {...document};

function resolveRefsRecursive(obj: RecursiveType) {
if (Array.isArray(obj)) {
for (const item of obj) {
resolveRefsRecursive(item);
}
} else if (typeof obj === 'object') {
for (const key in obj) {
if (key === '$ref') {
const lookup = obj[key] as string;
// Lookup is a string that defines the path to the referenced object like '#/components/schemas/Example'
const path = lookup.split('/').slice(1);
// Find the referenced object in the document using lodash's get function
const referencedObject = get(resolvedDocument, path);

// Remove the $ref: '...' key from the object and replace it with the referenced object itself
obj = Object.assign(obj, referencedObject);
} else if (typeof obj[key] === 'object' || Array.isArray(obj[key])) {
const guarded = obj[key] as RecursiveType;
// If the value is an object or an array, recursively call this function
resolveRefsRecursive(guarded);
}
}
}
}

resolveRefsRecursive(resolvedDocument);

return resolvedDocument;
}
</script>
56 changes: 56 additions & 0 deletions src/.vuepress/theme/client/components/global/OpenApiExample.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<template>
<template v-if="example">
<strong class="inline-block text-sm">{{ title || 'Example' }}:</strong>&nbsp;
<Markdown
v-if="isExampleObject(example) && example.summary"
class="text-sm"
:content="example.summary" />

<Markdown
v-if="isExampleObject(example) && example.description"
class="text-sm"
:content="example.description" />

<CodeBlock
v-if="formattedExample && isMultilineString"
:code="formattedExample" />

<code v-else>{{ formattedExample }}</code>
</template>
</template>

<script setup lang="ts">
import {computed} from 'vue';
import {type OpenAPIV3_1 as OpenApiType} from 'openapi-types';
import Markdown from '@mptheme/client/components/global/Markdown.vue';
import CodeBlock from './CodeBlock.vue';

const props = defineProps<{
title?: string;
example?: OpenApiType.ExampleObject | unknown;
}>();

const formattedExample = computed(() => {
if (!props.example) return undefined;

if (isExampleObject(props.example)) return formatExample(props.example.value);

if (typeof props.example === 'object') return formatExample(props.example);

return props.example.toString();
});

// Check the formattedExample for linebreaks, if it has those, it's a multiline string
const isMultilineString = computed(() => formattedExample.value?.includes('\n'));

function formatExample(example: unknown): string {
FreekVR marked this conversation as resolved.
Show resolved Hide resolved
const spacing = 2;

return JSON.stringify(example, null, spacing);
}

// Guard to check if example is an Example object
const isExampleObject = (example: unknown): example is OpenApiType.ExampleObject => {
return !!example && typeof example === 'object' && 'value' in example;
};
</script>
20 changes: 20 additions & 0 deletions src/.vuepress/theme/client/components/global/OpenApiInfo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<section class="open-api-info">
<hgroup>
<h2>{{ info.title }}</h2>
<p>(v{{ info.version }})</p>
<Markdown
v-if="info.description"
:content="info.description" />
</hgroup>
</section>
</template>

<script setup lang="ts">
import {type OpenAPIV3_1 as OpenApiType} from 'openapi-types';
import Markdown from './Markdown.vue';

defineProps<{
info: OpenApiType.Document['info'];
}>();
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<template>
<header class="bg-gray-100 dark:bg-gray-800 mb-4 mt-4 open-api-operation p-3">
<strong>Endpoint:</strong>
<pre
class="dark:text-black inline-block leading-none m-0 ml-3 p-1 rounded-sm text-sm"
:class="methodClass"
>{{ method.toUpperCase() }}</pre
>
FreekVR marked this conversation as resolved.
Show resolved Hide resolved
<pre class="dark:text-gray-100 inline m-0 ml-2 p-0 text-gray-700 text-sm">{{ endpoint }}</pre>
<br />

<template v-if="securityRequirements?.length && securitySchemes">
<strong>Authentication:</strong>&nbsp;
<OpenApiSecurityRequirements
class="text-sm"
:security="securityRequirements"
:security-schemes="securitySchemes" />
</template>
</header>
</template>

<script setup lang="ts">
import {computed} from 'vue';
import {type OpenAPIV3_1 as OpenApiType} from 'openapi-types';
import OpenApiSecurityRequirements from '@mptheme/client/components/global/OpenApiSecurityRequirements.vue';

const props = defineProps<{
method: string;
endpoint: string;
securityRequirements?: OpenApiType.SecurityRequirementObject[];
securitySchemes?: Record<string, OpenApiType.SecuritySchemeObject>;
}>();

const methodClass = computed(() => {
return {
'bg-green-200': props.method === 'get',
'bg-blue-200': props.method === 'post',
'bg-yellow-200': props.method === 'put',
'bg-red-200': props.method === 'delete',
'bg-purple-200': props.method === 'patch',
'bg-gray-200': props.method === 'options',
'bg-indigo-200': props.method === 'head',
'bg-pink-200': props.method === 'trace',
FreekVR marked this conversation as resolved.
Show resolved Hide resolved
};
});
</script>
Loading
Loading