- Notifications
You must be signed in to change notification settings - Fork 27
feat(spec): tag POC #435
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(spec): tag POC #435
Changes from 8 commits
7671b0a 9dbc271 432b8bb d16f2db 8ad4f6f 521b914 dd4f6e0 b8a8342 1cd50b7 4ae5f9e d2cefcc fd0ace9 6899303 de0c17a File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| | @@ -13,10 +13,17 @@ const ALGOLIASEARCH_LITE_OPERATIONS = [ | |
| 'post', | ||
| ]; | ||
| | ||
| async function propagateTagsToOperations( | ||
| bundledPath: string, | ||
| client: string | ||
| ): Promise<boolean> { | ||
| async function propagateTagsToOperations({ | ||
bodinsamuel marked this conversation as resolved. Show resolved Hide resolved | ||
| 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}.`); | ||
| } | ||
| | @@ -25,9 +32,36 @@ 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; | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why load the yaml twice ? Can you simply copy the file ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem in JS is pointer ref I can not simply reuse the same object. | ||
| 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 only have the clientName before open-api generator will use this to determine the name of the client | ||
| specMethod.tags = [clientName]; | ||
| | ||
| if (!withDoc || !bundledDocSpec!.paths[pathKey][method].tags) { | ||
bodinsamuel marked this conversation as resolved. Outdated Show resolved Hide resolved | ||
| continue; | ||
| } | ||
| | ||
| // Checks that specified tags are well defined at root level | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I there a reason we want to ensure that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. because if you don't define tags at the root level then nothing will appear in the doc There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will still default to the base tag so I guess it's fine, what I mean is this feature is for the doc and shouldn't prevent dev but it's just my opinion There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once it's done it's done, if you add a tag that means you expect to change the doc. So I'm not too worried about this impacting dev flow. | ||
| 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` | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| | ||
| | @@ -38,32 +72,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) { | ||
| | @@ -78,17 +118,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 ( | ||
| | @@ -105,95 +149,95 @@ 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, | ||
| 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'; | ||
| const specBase = isLite ? 'search' : spec; // In case of lite we use a different base because the base only exists virtually. | ||
bodinsamuel marked this conversation as resolved. Outdated Show resolved Hide resolved | ||
| 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.info(`checking cache for '${specBase}'`); | ||
| const generatedFiles: string[] = [`bundled/${spec}.yml`]; | ||
bodinsamuel marked this conversation as resolved. Outdated Show resolved Hide resolved | ||
| if (!isLite) { | ||
bodinsamuel marked this conversation as resolved. Outdated Show resolved Hide resolved | ||
| 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.info(`cache not found for '${specBase}'`); | ||
bodinsamuel marked this conversation as resolved. Outdated Show resolved Hide resolved | ||
| 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: true, | ||
bodinsamuel marked this conversation as resolved. Outdated Show resolved Hide resolved | ||
| }); | ||
| } 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( | ||
| | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,6 @@ | ||
| get: | ||
| tags: | ||
| - Indices | ||
| Comment on lines +2 to +3 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense to avoid any manual changes added in the specs, and default to the name of the folder the spec is stored in? This way, we only have to handle where to put the file, not what tag it should match There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So with today's structure, the tag for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nope because of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah sorry I did not fully explained why, but looking at the rest path, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we could but it would need more care in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They are both index operations so it wouldn't choc me to see both of them under an My point is only that we don't have extra logic but I get yours too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. one is index operation, the other record operation. it's definitely debatable but as a user I would find it weird :p | ||
| operationId: getTask | ||
| description: Check the current status of a given task. | ||
| summary: Check the current status of a given task. | ||
| | ||
Uh oh!
There was an error while loading. Please reload this page.