Skip to content

Commit

Permalink
feat(spec): tag POC (#435)
Browse files Browse the repository at this point in the history
  • Loading branch information
Samuel Bodin authored Apr 27, 2022
1 parent 74252ea commit 3d8c1b9
Show file tree
Hide file tree
Showing 50 changed files with 493 additions and 251 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ clients/algoliasearch-client-javascript/packages/*/.openapi-generator-ignore
tests/output/*/.openapi-generator-ignore

generators/bin
*.doc.yml
2 changes: 1 addition & 1 deletion netlify.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build]
command="yarn website:build"
command="BUNDLE_WITH_DOC=true DOCKER=true yarn cli build specs all && yarn website:build"
publish="website/build"
ignore="git diff --quiet $COMMIT_REF $CACHED_COMMIT_REF -- website/"

Expand Down
215 changes: 136 additions & 79 deletions scripts/buildSpecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import fsp from 'fs/promises';

import yaml from 'js-yaml';

import { checkForCache, exists, run, toAbsolutePath } from './common';
import {
BUNDLE_WITH_DOC,
checkForCache,
exists,
run,
toAbsolutePath,
} from './common';
import { createSpinner } from './oraLog';
import type { Spec } from './types';

Expand All @@ -13,10 +19,17 @@ const ALGOLIASEARCH_LITE_OPERATIONS = [
'post',
];

async function propagateTagsToOperations(
bundledPath: string,
client: string
): Promise<boolean> {
async function propagateTagsToOperations({
bundledPath,
withDoc,
clientName,
alias,
}: {
bundledPath: string;
withDoc: boolean;
clientName: string;
alias?: string;
}): Promise<void> {
if (!(await exists(bundledPath))) {
throw new Error(`Bundled file not found ${bundledPath}.`);
}
Expand All @@ -25,9 +38,41 @@ async function propagateTagsToOperations(
await fsp.readFile(bundledPath, 'utf8')
) as Spec;

for (const pathMethods of Object.values(bundledSpec.paths)) {
for (const specMethod of Object.values(pathMethods)) {
specMethod.tags = [client];
let bundledDocSpec: Spec | undefined;
if (withDoc) {
bundledDocSpec = yaml.load(await fsp.readFile(bundledPath, 'utf8')) as Spec;
}
const tagsDefinitions = bundledSpec.tags;

for (const [pathKey, pathMethods] of Object.entries(bundledSpec.paths)) {
for (const [method, specMethod] of Object.entries(pathMethods)) {
// 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];

if (
!withDoc ||
!bundledDocSpec ||
!bundledDocSpec.paths[pathKey][method].tags
) {
continue;
}

// Checks that specified tags are well defined at root level
for (const tag of bundledDocSpec.paths[pathKey][method].tags) {
if (tag === clientName || (alias && tag === alias)) {
return;
}

const tagExists = tagsDefinitions
? tagsDefinitions.find((t) => t.name === tag)
: null;
if (!tagExists) {
throw new Error(
`Tag "${tag}" in "client[${clientName}] -> operation[${specMethod.operationId}]" is not defined`
);
}
}
}
}

Expand All @@ -38,32 +83,38 @@ async function propagateTagsToOperations(
})
);

return true;
if (withDoc) {
const pathToDoc = bundledPath.replace('.yml', '.doc.yml');
await fsp.writeFile(
pathToDoc,
yaml.dump(bundledDocSpec, {
noRefs: true,
})
);
}
}

async function lintCommon(verbose: boolean, useCache: boolean): Promise<void> {
const spinner = createSpinner('linting common spec', verbose).start();

let hash = '';
const cacheFile = toAbsolutePath(`specs/dist/common.cache`);
if (useCache) {
const { cacheExists, hash: newCache } = await checkForCache(
{
job: 'common specs',
folder: toAbsolutePath('specs/'),
generatedFiles: [],
filesToCache: ['common'],
cacheFile,
},
verbose
);
const { cacheExists, hash: newCache } = await checkForCache({
folder: toAbsolutePath('specs/'),
generatedFiles: [],
filesToCache: ['common'],
cacheFile,
});

if (cacheExists) {
spinner.succeed("job skipped, cache found for 'common' spec");
return;
}

hash = newCache;
}

const spinner = createSpinner('linting common spec', verbose).start();
await run(`yarn specs:lint common`, { verbose });

if (hash) {
Expand All @@ -78,17 +129,21 @@ async function lintCommon(verbose: boolean, useCache: boolean): Promise<void> {
* Creates a lite search spec with the `ALGOLIASEARCH_LITE_OPERATIONS` methods
* from the `search` spec.
*/
async function buildLiteSpec(
spec: string,
bundledPath: string,
outputFormat: string,
verbose: boolean
): Promise<void> {
const searchSpec = yaml.load(
async function buildLiteSpec({
spec,
bundledPath,
outputFormat,
}: {
spec: string;
bundledPath: string;
outputFormat: string;
}): Promise<void> {
const parsed = yaml.load(
await fsp.readFile(toAbsolutePath(bundledPath), 'utf8')
) as Spec;

searchSpec.paths = Object.entries(searchSpec.paths).reduce(
// Filter methods.
parsed.paths = Object.entries(parsed.paths).reduce(
(acc, [path, operations]) => {
for (const [method, operation] of Object.entries(operations)) {
if (
Expand All @@ -105,95 +160,97 @@ async function buildLiteSpec(
);

const liteBundledPath = `specs/bundled/${spec}.${outputFormat}`;
await fsp.writeFile(toAbsolutePath(liteBundledPath), yaml.dump(searchSpec));

if (
!(await propagateTagsToOperations(toAbsolutePath(liteBundledPath), spec))
) {
throw new Error(
`Unable to propage tags to operations for \`${spec}\` spec.`
);
}
await fsp.writeFile(toAbsolutePath(liteBundledPath), yaml.dump(parsed));

await run(`yarn specs:fix bundled/${spec}.${outputFormat}`, {
verbose,
await propagateTagsToOperations({
bundledPath: toAbsolutePath(liteBundledPath),
clientName: spec,
// Lite does not need documentation because it's just a subset
withDoc: false,
});
}

/**
* Build spec file.
*/
async function buildSpec(
spec: string,
outputFormat: string,
verbose: boolean,
useCache: boolean
): Promise<void> {
const shouldBundleLiteSpec = spec === 'algoliasearch-lite';
const client = shouldBundleLiteSpec ? 'search' : spec;
const cacheFile = toAbsolutePath(`specs/dist/${client}.cache`);
const isLite = spec === 'algoliasearch-lite';
// In case of lite we use a the `search` spec as a base because only its bundled form exists.
const specBase = isLite ? 'search' : spec;
const cacheFile = toAbsolutePath(`specs/dist/${spec}.cache`);
let hash = '';

createSpinner(`'${client}' spec`, verbose).start().info();
const spinner = createSpinner(`starting '${spec}' spec`, verbose).start();

if (useCache) {
const generatedFiles = [`bundled/${client}.yml`];

if (shouldBundleLiteSpec) {
generatedFiles.push(`bundled/${spec}.yml`);
spinner.text = `checking cache for '${specBase}'`;
const generatedFiles = [`bundled/${spec}.yml`];
if (!isLite && BUNDLE_WITH_DOC) {
generatedFiles.push(`bundled/${spec}.doc.yml`);
}

const { cacheExists, hash: newCache } = await checkForCache(
{
job: `'${client}' specs`,
folder: toAbsolutePath('specs/'),
generatedFiles,
filesToCache: [client, 'common'],
cacheFile,
},
verbose
);
const { cacheExists, hash: newCache } = await checkForCache({
folder: toAbsolutePath('specs/'),
generatedFiles,
filesToCache: [specBase, 'common'],
cacheFile,
});

if (cacheExists) {
spinner.succeed(`job skipped, cache found for '${specBase}'`);
return;
}

spinner.text = `cache not found for '${specBase}'`;
hash = newCache;
}

const spinner = createSpinner(`building ${client} spec`, verbose).start();
const bundledPath = `specs/bundled/${client}.${outputFormat}`;
// First linting the base
spinner.text = `linting '${spec}' spec`;
await run(`yarn specs:fix ${specBase}`, { verbose });

// Then bundle the file
const bundledPath = `specs/bundled/${spec}.${outputFormat}`;
await run(
`yarn openapi bundle specs/${client}/spec.yml -o ${bundledPath} --ext ${outputFormat}`,
`yarn openapi bundle specs/${specBase}/spec.yml -o ${bundledPath} --ext ${outputFormat}`,
{ verbose }
);

if (!(await propagateTagsToOperations(toAbsolutePath(bundledPath), client))) {
spinner.fail();
throw new Error(
`Unable to propage tags to operations for \`${client}\` spec.`
);
// Add the correct tags to be able to generate the proper client
if (!isLite) {
await propagateTagsToOperations({
bundledPath: toAbsolutePath(bundledPath),
clientName: spec,
withDoc: BUNDLE_WITH_DOC,
});
} else {
await buildLiteSpec({
spec,
bundledPath: toAbsolutePath(bundledPath),
outputFormat,
});
}

spinner.text = `linting ${client} spec`;
await run(`yarn specs:fix ${client}`, { verbose });

spinner.text = `validating ${client} spec`;
await run(`yarn openapi lint specs/bundled/${client}.${outputFormat}`, {
// Validate and lint the final bundle
spinner.text = `validating '${spec}' bundled spec`;
await run(`yarn openapi lint specs/bundled/${spec}.${outputFormat}`, {
verbose,
});

spinner.text = `linting '${client}' bundled spec`;
await run(`yarn specs:fix bundled/${client}.${outputFormat}`, { verbose });

if (shouldBundleLiteSpec) {
spinner.text = `Building and linting '${spec}' spec`;
await buildLiteSpec(spec, bundledPath, outputFormat, verbose);
}
spinner.text = `linting '${spec}' bundled spec`;
await run(`yarn specs:fix bundled/${spec}.${outputFormat}`, { verbose });

if (hash) {
spinner.text = `storing ${client} spec cache`;
spinner.text = `storing '${spec}' spec cache`;
await fsp.writeFile(cacheFile, hash);
}

spinner.succeed(`building complete for '${client}' spec`);
spinner.succeed(`building complete for '${spec}' spec`);
}

export async function buildSpecs(
Expand Down
Loading

0 comments on commit 3d8c1b9

Please sign in to comment.