Skip to content

Commit 3d8c1b9

Browse files
author
Samuel Bodin
authored
feat(spec): tag POC (#435)
1 parent 74252ea commit 3d8c1b9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+493
-251
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ clients/algoliasearch-client-javascript/packages/*/.openapi-generator-ignore
2828
tests/output/*/.openapi-generator-ignore
2929

3030
generators/bin
31+
*.doc.yml

netlify.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[build]
2-
command="yarn website:build"
2+
command="BUNDLE_WITH_DOC=true DOCKER=true yarn cli build specs all && yarn website:build"
33
publish="website/build"
44
ignore="git diff --quiet $COMMIT_REF $CACHED_COMMIT_REF -- website/"
55

scripts/buildSpecs.ts

Lines changed: 136 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import fsp from 'fs/promises';
22

33
import yaml from 'js-yaml';
44

5-
import { checkForCache, exists, run, toAbsolutePath } from './common';
5+
import {
6+
BUNDLE_WITH_DOC,
7+
checkForCache,
8+
exists,
9+
run,
10+
toAbsolutePath,
11+
} from './common';
612
import { createSpinner } from './oraLog';
713
import type { Spec } from './types';
814

@@ -13,10 +19,17 @@ const ALGOLIASEARCH_LITE_OPERATIONS = [
1319
'post',
1420
];
1521

16-
async function propagateTagsToOperations(
17-
bundledPath: string,
18-
client: string
19-
): Promise<boolean> {
22+
async function propagateTagsToOperations({
23+
bundledPath,
24+
withDoc,
25+
clientName,
26+
alias,
27+
}: {
28+
bundledPath: string;
29+
withDoc: boolean;
30+
clientName: string;
31+
alias?: string;
32+
}): Promise<void> {
2033
if (!(await exists(bundledPath))) {
2134
throw new Error(`Bundled file not found ${bundledPath}.`);
2235
}
@@ -25,9 +38,41 @@ async function propagateTagsToOperations(
2538
await fsp.readFile(bundledPath, 'utf8')
2639
) as Spec;
2740

28-
for (const pathMethods of Object.values(bundledSpec.paths)) {
29-
for (const specMethod of Object.values(pathMethods)) {
30-
specMethod.tags = [client];
41+
let bundledDocSpec: Spec | undefined;
42+
if (withDoc) {
43+
bundledDocSpec = yaml.load(await fsp.readFile(bundledPath, 'utf8')) as Spec;
44+
}
45+
const tagsDefinitions = bundledSpec.tags;
46+
47+
for (const [pathKey, pathMethods] of Object.entries(bundledSpec.paths)) {
48+
for (const [method, specMethod] of Object.entries(pathMethods)) {
49+
// In the main bundle we need to have only the clientName
50+
// because open-api-generator will use this to determine the name of the client
51+
specMethod.tags = [clientName];
52+
53+
if (
54+
!withDoc ||
55+
!bundledDocSpec ||
56+
!bundledDocSpec.paths[pathKey][method].tags
57+
) {
58+
continue;
59+
}
60+
61+
// Checks that specified tags are well defined at root level
62+
for (const tag of bundledDocSpec.paths[pathKey][method].tags) {
63+
if (tag === clientName || (alias && tag === alias)) {
64+
return;
65+
}
66+
67+
const tagExists = tagsDefinitions
68+
? tagsDefinitions.find((t) => t.name === tag)
69+
: null;
70+
if (!tagExists) {
71+
throw new Error(
72+
`Tag "${tag}" in "client[${clientName}] -> operation[${specMethod.operationId}]" is not defined`
73+
);
74+
}
75+
}
3176
}
3277
}
3378

@@ -38,32 +83,38 @@ async function propagateTagsToOperations(
3883
})
3984
);
4085

41-
return true;
86+
if (withDoc) {
87+
const pathToDoc = bundledPath.replace('.yml', '.doc.yml');
88+
await fsp.writeFile(
89+
pathToDoc,
90+
yaml.dump(bundledDocSpec, {
91+
noRefs: true,
92+
})
93+
);
94+
}
4295
}
4396

4497
async function lintCommon(verbose: boolean, useCache: boolean): Promise<void> {
98+
const spinner = createSpinner('linting common spec', verbose).start();
99+
45100
let hash = '';
46101
const cacheFile = toAbsolutePath(`specs/dist/common.cache`);
47102
if (useCache) {
48-
const { cacheExists, hash: newCache } = await checkForCache(
49-
{
50-
job: 'common specs',
51-
folder: toAbsolutePath('specs/'),
52-
generatedFiles: [],
53-
filesToCache: ['common'],
54-
cacheFile,
55-
},
56-
verbose
57-
);
103+
const { cacheExists, hash: newCache } = await checkForCache({
104+
folder: toAbsolutePath('specs/'),
105+
generatedFiles: [],
106+
filesToCache: ['common'],
107+
cacheFile,
108+
});
58109

59110
if (cacheExists) {
111+
spinner.succeed("job skipped, cache found for 'common' spec");
60112
return;
61113
}
62114

63115
hash = newCache;
64116
}
65117

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

69120
if (hash) {
@@ -78,17 +129,21 @@ async function lintCommon(verbose: boolean, useCache: boolean): Promise<void> {
78129
* Creates a lite search spec with the `ALGOLIASEARCH_LITE_OPERATIONS` methods
79130
* from the `search` spec.
80131
*/
81-
async function buildLiteSpec(
82-
spec: string,
83-
bundledPath: string,
84-
outputFormat: string,
85-
verbose: boolean
86-
): Promise<void> {
87-
const searchSpec = yaml.load(
132+
async function buildLiteSpec({
133+
spec,
134+
bundledPath,
135+
outputFormat,
136+
}: {
137+
spec: string;
138+
bundledPath: string;
139+
outputFormat: string;
140+
}): Promise<void> {
141+
const parsed = yaml.load(
88142
await fsp.readFile(toAbsolutePath(bundledPath), 'utf8')
89143
) as Spec;
90144

91-
searchSpec.paths = Object.entries(searchSpec.paths).reduce(
145+
// Filter methods.
146+
parsed.paths = Object.entries(parsed.paths).reduce(
92147
(acc, [path, operations]) => {
93148
for (const [method, operation] of Object.entries(operations)) {
94149
if (
@@ -105,95 +160,97 @@ async function buildLiteSpec(
105160
);
106161

107162
const liteBundledPath = `specs/bundled/${spec}.${outputFormat}`;
108-
await fsp.writeFile(toAbsolutePath(liteBundledPath), yaml.dump(searchSpec));
109-
110-
if (
111-
!(await propagateTagsToOperations(toAbsolutePath(liteBundledPath), spec))
112-
) {
113-
throw new Error(
114-
`Unable to propage tags to operations for \`${spec}\` spec.`
115-
);
116-
}
163+
await fsp.writeFile(toAbsolutePath(liteBundledPath), yaml.dump(parsed));
117164

118-
await run(`yarn specs:fix bundled/${spec}.${outputFormat}`, {
119-
verbose,
165+
await propagateTagsToOperations({
166+
bundledPath: toAbsolutePath(liteBundledPath),
167+
clientName: spec,
168+
// Lite does not need documentation because it's just a subset
169+
withDoc: false,
120170
});
121171
}
122172

173+
/**
174+
* Build spec file.
175+
*/
123176
async function buildSpec(
124177
spec: string,
125178
outputFormat: string,
126179
verbose: boolean,
127180
useCache: boolean
128181
): Promise<void> {
129-
const shouldBundleLiteSpec = spec === 'algoliasearch-lite';
130-
const client = shouldBundleLiteSpec ? 'search' : spec;
131-
const cacheFile = toAbsolutePath(`specs/dist/${client}.cache`);
182+
const isLite = spec === 'algoliasearch-lite';
183+
// In case of lite we use a the `search` spec as a base because only its bundled form exists.
184+
const specBase = isLite ? 'search' : spec;
185+
const cacheFile = toAbsolutePath(`specs/dist/${spec}.cache`);
132186
let hash = '';
133187

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

136190
if (useCache) {
137-
const generatedFiles = [`bundled/${client}.yml`];
138-
139-
if (shouldBundleLiteSpec) {
140-
generatedFiles.push(`bundled/${spec}.yml`);
191+
spinner.text = `checking cache for '${specBase}'`;
192+
const generatedFiles = [`bundled/${spec}.yml`];
193+
if (!isLite && BUNDLE_WITH_DOC) {
194+
generatedFiles.push(`bundled/${spec}.doc.yml`);
141195
}
142196

143-
const { cacheExists, hash: newCache } = await checkForCache(
144-
{
145-
job: `'${client}' specs`,
146-
folder: toAbsolutePath('specs/'),
147-
generatedFiles,
148-
filesToCache: [client, 'common'],
149-
cacheFile,
150-
},
151-
verbose
152-
);
197+
const { cacheExists, hash: newCache } = await checkForCache({
198+
folder: toAbsolutePath('specs/'),
199+
generatedFiles,
200+
filesToCache: [specBase, 'common'],
201+
cacheFile,
202+
});
153203

154204
if (cacheExists) {
205+
spinner.succeed(`job skipped, cache found for '${specBase}'`);
155206
return;
156207
}
157208

209+
spinner.text = `cache not found for '${specBase}'`;
158210
hash = newCache;
159211
}
160212

161-
const spinner = createSpinner(`building ${client} spec`, verbose).start();
162-
const bundledPath = `specs/bundled/${client}.${outputFormat}`;
213+
// First linting the base
214+
spinner.text = `linting '${spec}' spec`;
215+
await run(`yarn specs:fix ${specBase}`, { verbose });
216+
217+
// Then bundle the file
218+
const bundledPath = `specs/bundled/${spec}.${outputFormat}`;
163219
await run(
164-
`yarn openapi bundle specs/${client}/spec.yml -o ${bundledPath} --ext ${outputFormat}`,
220+
`yarn openapi bundle specs/${specBase}/spec.yml -o ${bundledPath} --ext ${outputFormat}`,
165221
{ verbose }
166222
);
167223

168-
if (!(await propagateTagsToOperations(toAbsolutePath(bundledPath), client))) {
169-
spinner.fail();
170-
throw new Error(
171-
`Unable to propage tags to operations for \`${client}\` spec.`
172-
);
224+
// Add the correct tags to be able to generate the proper client
225+
if (!isLite) {
226+
await propagateTagsToOperations({
227+
bundledPath: toAbsolutePath(bundledPath),
228+
clientName: spec,
229+
withDoc: BUNDLE_WITH_DOC,
230+
});
231+
} else {
232+
await buildLiteSpec({
233+
spec,
234+
bundledPath: toAbsolutePath(bundledPath),
235+
outputFormat,
236+
});
173237
}
174238

175-
spinner.text = `linting ${client} spec`;
176-
await run(`yarn specs:fix ${client}`, { verbose });
177-
178-
spinner.text = `validating ${client} spec`;
179-
await run(`yarn openapi lint specs/bundled/${client}.${outputFormat}`, {
239+
// Validate and lint the final bundle
240+
spinner.text = `validating '${spec}' bundled spec`;
241+
await run(`yarn openapi lint specs/bundled/${spec}.${outputFormat}`, {
180242
verbose,
181243
});
182244

183-
spinner.text = `linting '${client}' bundled spec`;
184-
await run(`yarn specs:fix bundled/${client}.${outputFormat}`, { verbose });
185-
186-
if (shouldBundleLiteSpec) {
187-
spinner.text = `Building and linting '${spec}' spec`;
188-
await buildLiteSpec(spec, bundledPath, outputFormat, verbose);
189-
}
245+
spinner.text = `linting '${spec}' bundled spec`;
246+
await run(`yarn specs:fix bundled/${spec}.${outputFormat}`, { verbose });
190247

191248
if (hash) {
192-
spinner.text = `storing ${client} spec cache`;
249+
spinner.text = `storing '${spec}' spec cache`;
193250
await fsp.writeFile(cacheFile, hash);
194251
}
195252

196-
spinner.succeed(`building complete for '${client}' spec`);
253+
spinner.succeed(`building complete for '${spec}' spec`);
197254
}
198255

199256
export async function buildSpecs(

0 commit comments

Comments
 (0)