Skip to content

Commit

Permalink
docs: enrich samples with parameters and responses
Browse files Browse the repository at this point in the history
  • Loading branch information
shortcuts committed Nov 20, 2024
1 parent 2e894c9 commit 8bff736
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 93 deletions.
66 changes: 17 additions & 49 deletions scripts/specs/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import { HarRequest, HTTPSnippet } from 'httpsnippet';
import yaml from 'js-yaml';

import { Cache } from '../cache.js';
import { exists, GENERATORS, run, toAbsolutePath } from '../common.js';
import { GENERATORS, run, toAbsolutePath } from '../common.js';
import { createSpinner } from '../spinners.js';
import type { Spec } from '../types.js';

import { getCodeSampleLabel, transformCodeSamplesToGuideMethods, transformSnippetsToCodeSamples } from './snippets.js';
import type { SnippetSamples } from './types.js';
import { bundleCodeSamplesForDoc, getCodeSampleLabel, transformGeneratedSnippetsToCodeSamples } from './snippets.js';

export async function lintCommon(useCache: boolean): Promise<void> {
const spinner = createSpinner('linting common spec');
Expand All @@ -37,48 +36,26 @@ export async function lintCommon(useCache: boolean): Promise<void> {
spinner.succeed();
}

/*
* This function will transform properties in the bundle depending on the context.
* E.g:
* - Check tags definition
* - Add name of the client in tags
* - Remove unecessary punctuation for documentation
* - etc...
*/
export async function transformBundle({
bundledPath,
docs,
clientName,
alias,
}: {
bundledPath: string;
docs: boolean;
clientName: string;
alias?: string;
}): Promise<void> {
if (!(await exists(bundledPath))) {
throw new Error(`Bundled file not found ${bundledPath}.`);
}
export async function bundleSpecsForClient(bundledPath: string, clientName: string): Promise<void> {
const bundledSpec = yaml.load(await fsp.readFile(bundledPath, 'utf8')) as Spec;

Object.values(bundledSpec.paths).forEach((pathMethods) => {
Object.values(pathMethods).forEach((specMethod) => (specMethod.tags = [clientName]));
});

await fsp.writeFile(bundledPath, yaml.dump(bundledSpec, { noRefs: true }));
}

export async function bundleSpecsForDoc(bundledPath: string, clientName: string): Promise<void> {
const bundledSpec = yaml.load(await fsp.readFile(bundledPath, 'utf8')) as Spec;
const harRequests = await oas2har.oas2har(bundledSpec as any, { includeVendorExamples: true });
const tagsDefinitions = bundledSpec.tags;
const snippetSamples = docs ? await transformSnippetsToCodeSamples(clientName) : ({} as SnippetSamples);
const codeSamples = await transformGeneratedSnippetsToCodeSamples(clientName);

if (docs) {
const snippets = transformCodeSamplesToGuideMethods(JSON.parse(JSON.stringify(snippetSamples)));
await fsp.writeFile(toAbsolutePath(`docs/bundled/${clientName}-snippets.json`), snippets);
}
await bundleCodeSamplesForDoc(JSON.parse(JSON.stringify(codeSamples)), clientName);

for (const [pathKey, pathMethods] of Object.entries(bundledSpec.paths)) {
for (const [method, specMethod] of Object.entries(pathMethods)) {
if (!docs) {
// In the main bundle we need to have only the clientName
// because open-api-generator will use this to determine the name of the client
specMethod.tags = [clientName];
continue;
}

if (specMethod['x-helper']) {
delete bundledSpec.paths[pathKey];
break;
Expand All @@ -94,11 +71,11 @@ export async function transformBundle({
specMethod['x-codeSamples'] = [];
}

if (snippetSamples[gen.language][specMethod.operationId]) {
if (codeSamples[gen.language][specMethod.operationId]) {
specMethod['x-codeSamples'].push({
lang: gen.language,
label: getCodeSampleLabel(gen.language),
source: Object.values(snippetSamples[gen.language][specMethod.operationId])[0],
source: Object.values(codeSamples[gen.language][specMethod.operationId])[0],
});
}
}
Expand Down Expand Up @@ -142,12 +119,6 @@ export async function transformBundle({
);
}

if (alias && tag === alias) {
throw new Error(
`Tag name "${tag} for operation ${specMethod.operationId} must be different from alias ${alias}`,
);
}

const tagExists = tagsDefinitions ? tagsDefinitions.find((t) => t.name === tag) : null;
if (!tagExists) {
throw new Error(
Expand All @@ -158,8 +129,5 @@ export async function transformBundle({
}
}

await fsp.writeFile(
docs ? toAbsolutePath(`specs/bundled/${clientName}.doc.yml`) : bundledPath,
yaml.dump(bundledSpec, { noRefs: true }),
);
await fsp.writeFile(toAbsolutePath(`specs/bundled/${clientName}.doc.yml`), yaml.dump(bundledSpec, { noRefs: true }));
}
33 changes: 14 additions & 19 deletions scripts/specs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import fsp from 'fs/promises';
import yaml from 'js-yaml';

import { Cache } from '../cache.js';
import { run, toAbsolutePath } from '../common.js';
import { exists, run, toAbsolutePath } from '../common.js';
import { createSpinner } from '../spinners.js';
import type { Spec } from '../types.js';

import { lintCommon, transformBundle } from './format.js';
import { bundleSpecsForClient, bundleSpecsForDoc, lintCommon } from './format.js';
import type { BaseBuildSpecsOptions } from './types.js';

const ALGOLIASEARCH_LITE_OPERATIONS = ['search', 'customPost', 'getRecommendations'];
Expand Down Expand Up @@ -53,12 +53,7 @@ async function buildLiteSpec({
// remove unused components for the outputted light spec
await run(`yarn openapi bundle ${bundledPath} -o ${bundledPath} --ext yml --remove-unused-components`);

await transformBundle({
bundledPath,
clientName: spec,
// Lite does not need documentation because it's just a subset
docs: false,
});
await bundleSpecsForClient(bundledPath, spec);
}

/**
Expand All @@ -70,15 +65,15 @@ async function buildSpec({
docs,
useCache,
}: BaseBuildSpecsOptions & { spec: string }): Promise<void> {
const isAlgoliasearch = spec === 'algoliasearch';
const isLiteSpec = spec === 'algoliasearch';

if (docs && isAlgoliasearch) {
if (docs && isLiteSpec) {
return;
}

// In case of lite we use a the `search` spec as a base because only its bundled form exists.
const specBase = isAlgoliasearch ? 'search' : spec;
const deps = isAlgoliasearch ? ['search', 'recommend'] : [spec];
const specBase = isLiteSpec ? 'search' : spec;
const deps = isLiteSpec ? ['search', 'recommend'] : [spec];
const logSuffix = docs ? 'doc spec' : 'spec';
const cache = new Cache({
folder: toAbsolutePath('specs/'),
Expand All @@ -105,16 +100,16 @@ async function buildSpec({
await run(`yarn specs:fix ${specBase}`);

// Then bundle the file
const bundledPath = `specs/bundled/${spec}.${docs ? 'doc.' : ''}${outputFormat}`;
const bundledPath = toAbsolutePath(`specs/bundled/${spec}.${docs ? 'doc.' : ''}${outputFormat}`);
await run(`yarn openapi bundle specs/${specBase}/spec.yml -o ${bundledPath} --ext ${outputFormat}`);

if (!(await exists(bundledPath))) {
throw new Error(`Bundled file not found ${bundledPath}.`);
}

// Add the correct tags to be able to generate the proper client
if (!isAlgoliasearch) {
await transformBundle({
bundledPath: toAbsolutePath(bundledPath),
clientName: spec,
docs,
});
if (!isLiteSpec) {
docs ? await bundleSpecsForDoc(bundledPath, spec) : await bundleSpecsForClient(bundledPath, spec);
} else {
await buildLiteSpec({
spec,
Expand Down
44 changes: 22 additions & 22 deletions scripts/specs/snippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import fsp from 'fs/promises';
import { GENERATORS, capitalize, createClientName, exists, toAbsolutePath } from '../common.js';
import type { Language } from '../types.js';

import type { CodeSamples, SnippetForMethod, SnippetSamples } from './types.js';
import type { CodeSamples, OpenAPICodeSample, SampleForOperation } from './types.js';

export function getCodeSampleLabel(language: Language): CodeSamples['label'] {
export function getCodeSampleLabel(language: Language): OpenAPICodeSample['label'] {
switch (language) {
case 'csharp':
return 'C#';
Expand All @@ -14,14 +14,14 @@ export function getCodeSampleLabel(language: Language): CodeSamples['label'] {
case 'php':
return 'PHP';
default:
return capitalize(language) as CodeSamples['label'];
return capitalize(language) as OpenAPICodeSample['label'];
}
}

// Iterates over the snippet samples and sanitize the data to only keep the method part in order to use it in the guides.
export function transformCodeSamplesToGuideMethods(snippetSamples: SnippetSamples): string {
for (const [language, operationWithSample] of Object.entries(snippetSamples)) {
for (const [operation, samples] of Object.entries(operationWithSample)) {
// Iterates over the result of `transformSnippetsToCodeSamples` in order to generate a JSON file for the doc to consume.
export async function bundleCodeSamplesForDoc(codeSamples: CodeSamples, clientName: string): Promise<void> {
for (const [language, operationWithSamples] of Object.entries(codeSamples)) {
for (const [operation, samples] of Object.entries(operationWithSamples)) {
if (operation === 'import') {
continue;
}
Expand All @@ -37,25 +37,25 @@ export function transformCodeSamplesToGuideMethods(snippetSamples: SnippetSample
const initLine = sampleMatch[1];
const callLine = sampleMatch[3];

if (!('init' in snippetSamples[language])) {
snippetSamples[language].init = {
if (!('init' in codeSamples[language])) {
codeSamples[language].init = {
default: initLine.trim(),
};
}

snippetSamples[language][operation][sampleName] = callLine.trim();
codeSamples[language][operation][sampleName] = callLine.trim();
}
}
}

return JSON.stringify(snippetSamples, null, 2);
await fsp.writeFile(toAbsolutePath(`docs/bundled/${clientName}-snippets.json`), JSON.stringify(codeSamples, null, 2));
}

// For a given `clientName`, reads the matching snippet file for every available clients and builds an hashmap of snippets per operationId per language.
export async function transformSnippetsToCodeSamples(clientName: string): Promise<SnippetSamples> {
const snippetSamples = Object.values(GENERATORS).reduce(
// Reads the generated `docs/snippets/` file for every languages of the given `clientName` and builds an hashmap of snippets per operationId per language.
export async function transformGeneratedSnippetsToCodeSamples(clientName: string): Promise<CodeSamples> {
const codeSamples = Object.values(GENERATORS).reduce<CodeSamples>(
(prev, curr) => ({ ...prev, [curr.language]: {} }),
{} as SnippetSamples,
{} as CodeSamples,
);

for (const gen of Object.values(GENERATORS)) {
Expand All @@ -76,7 +76,7 @@ export async function transformSnippetsToCodeSamples(clientName: string): Promis

const importMatch = snippetFileContent.match(/>IMPORT\n([\s\S]*?)\n.*IMPORT</);
if (importMatch) {
snippetSamples[gen.language].import = {
codeSamples[gen.language].import = {
default: importMatch[1].trim(),
};
}
Expand All @@ -91,24 +91,24 @@ export async function transformSnippetsToCodeSamples(clientName: string): Promis
const operationId = match[1];
const testName = match[2] || 'default';

if (!snippetSamples[gen.language][operationId]) {
snippetSamples[gen.language][operationId] = {};
if (!codeSamples[gen.language][operationId]) {
codeSamples[gen.language][operationId] = {};
}

const snippetForMethod: SnippetForMethod = snippetSamples[gen.language][operationId];
const sampleForOperation: SampleForOperation = codeSamples[gen.language][operationId];

snippetForMethod[testName] = '';
sampleForOperation[testName] = '';

const indent = lines[0].length - lines[0].trim().length;
// skip first and last lines because they contain the SEPARATOR or operationId
lines.forEach((line) => {
// best effort to determine how far the snippet is indented so we
// can have every snippets in the documentation on the far left
// without impacting the formatting
snippetForMethod[testName] += `${line.slice(indent).replaceAll(/\t/g, ' ')}\n`;
sampleForOperation[testName] += `${line.slice(indent).replaceAll(/\t/g, ' ')}\n`;
});
}
}

return snippetSamples;
return codeSamples;
}
6 changes: 3 additions & 3 deletions scripts/specs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ export type BaseBuildSpecsOptions = {
useCache: boolean;
};

export type SnippetForMethod = Record<string, string>;
export type SnippetSamples = Record<Language, Record<string, SnippetForMethod>>;
export type SampleForOperation = Record<string, string>;
export type CodeSamples = Record<Language, Record<string, SampleForOperation>>;

export type CodeSamples = {
export type OpenAPICodeSample = {
lang:
| 'c'
| 'c++'
Expand Down

0 comments on commit 8bff736

Please sign in to comment.