generator/autorest.ts (90 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import path from 'path'; import os from 'os'; import { findRecursive, executeCmd, fileExists } from './utils'; import * as constants from './constants'; import { readFile, writeFile } from 'fs/promises'; import * as markdown from '@ts-common/commonmark-to-markdown' import * as yaml from 'js-yaml' const autorestBinary = os.platform() === 'win32' ? 'autorest.cmd' : 'autorest'; const rootDir = `${__dirname}/../`; const extensionDir = path.resolve(`${rootDir}/bicep-types-az/src/autorest.bicep/`); function normalizeJsonPath(jsonPath: string) { // eslint-disable-next-line no-useless-escape return path.normalize(jsonPath).replace(/[\\\/]/g, '/'); } async function execAutoRest(tmpFolder: string, params: string[]) { await executeCmd(__dirname, `${__dirname}/node_modules/.bin/${autorestBinary}`, params); if (!fileExists(tmpFolder)) { return []; } return await findRecursive(tmpFolder, p => path.extname(p) === '.json'); } export async function runAutorest(readme: string, tmpFolder: string) { const autoRestParams = [ `--use=@autorest/modelerfour`, `--use=${extensionDir}`, '--bicep', `--output-folder=${tmpFolder}`, '--multiapi', '--title=none', // This is necessary to avoid failures such as "ERROR: Semantic violation: Discriminator must be a required property." blocking type generation. // In an ideal world, we'd raise issues in https://github.com/Azure/azure-rest-api-specs and force RP teams to fix them, but this isn't very practical // as new validations are added continuously, and there's often quite a lag before teams will fix them - we don't want to be blocked by this in generating types. `--skip-semantics-validation`, `--arm-schema=true`, readme, ]; if (constants.autoRestVerboseOutput) { autoRestParams.push('--verbose'); } return await execAutoRest(tmpFolder, autoRestParams); } export async function generateAutorestConfig(readmePath: string, bicepReadmePath: string) { // This function takes in an input autorest configuration file (readme.md), and generates a autorest configuration file tailored for use by autorest.bicep (readme.bicep.md) // We search for markdown yaml blocks containing input .json files, and unconditionally use them to generate output. // The expected output file should consist of a set of blocks tagged by api version and a 'multi-api' block with links to tags: // // ##Bicep // // ### Bicep multi-api // ```yaml $(bicep) && $(multiapi) // batch: // - tag: microsoft.securityinsights-2024-03-01 // - tag: microsoft.securityinsights-2024-01-01-preview // ... // ``` // // ### Tag: microsoft.securityinsights-2024-03-01 and bicep // ```yaml $(tag) == 'microsoft.securityinsights-2024-03-01' && $(bicep) // input-file: // - Microsoft.SecurityInsights/stable/2024-03-01/AlertRules.json // - Microsoft.SecurityInsights/stable/2024-03-01/AutomationRules.json // ... // ``` // // ### Tag: microsoft.securityinsights-2024-01-01-preview and bicep // ```yaml $(tag) == 'microsoft.securityinsights-2024-01-01-preview' && $(bicep) // input-file: // - Microsoft.SecurityInsights/preview/2024-01-01-preview/AlertRules.json // - Microsoft.SecurityInsights/preview/2024-01-01-preview/AutomationRules.json // - Microsoft.SecurityInsights/preview/2024-01-01-preview/BillingStatistics.json // ... // We expect a path format convention of <provider>/(any/number/of/intervening/folders)/<yyyy>-<mm>-<dd>(|-preview)/<filename>.json // This information is used to generate individual tags in the generated autorest configuration // eslint-disable-next-line no-useless-escape const pathRegex = /^(\$\(this-folder\)\/|)([^\/]+)(?:\/[^\/]+)*\/(\d{4}-\d{2}-\d{2}(|-preview))\/.*\.json$/i; const readmeContents = await readFile(readmePath, { encoding: 'utf8' }); const readmeMarkdown = markdown.parse(readmeContents); const inputFiles = new Set<string>(); // we need to look for all autorest configuration elements containing input files, and collect that list of files. These will look like (e.g.): // ```yaml $(tag) == 'someTag' // input-file: // - path/to/file.json // - path/to/other_file.json // ``` for (const node of markdown.iterate(readmeMarkdown.markDown)) { // We're only interested in yaml code blocks if (node.type !== 'code_block' || !node.info || !node.literal || !node.info.trim().startsWith('yaml')) { continue; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const yamlData = yaml.load(node.literal) as any; if (yamlData) { // input-file may be a single string or an array of strings const inputFile = yamlData['input-file']; if (typeof inputFile === 'string') { inputFiles.add(inputFile); } else if (inputFile instanceof Array) { for (const i of inputFile) { inputFiles.add(i); } } } } const filesByTag: Record<string, string[]> = {}; for (const file of inputFiles) { const normalizedFile = normalizeJsonPath(file); const match = pathRegex.exec(normalizedFile); if (match) { // Generate a unique tag. We can't process all of the different API versions in one autorest pass // because there are constraints on naming uniqueness (e.g. naming of definitions), so we want to pass over // each API version separately. const tagName = `${match[2].toLowerCase()}-${match[3].toLowerCase()}`; if (!filesByTag[tagName]) { filesByTag[tagName] = []; } filesByTag[tagName].push(normalizedFile); } else { console.warn(`WARNING: Unable to parse swagger path "${file}"`); } } let generatedContent = `##Bicep ### Bicep multi-api \`\`\`yaml $(bicep) && $(multiapi) ${yaml.dump({ 'batch': Object.keys(filesByTag).map(tag => ({ 'tag': tag })) }, { lineWidth: 1000 })} \`\`\` `; for (const tag of Object.keys(filesByTag)) { generatedContent += `### Tag: ${tag} and bicep \`\`\`yaml $(tag) == '${tag}' && $(bicep) ${yaml.dump({ 'input-file': filesByTag[tag] }, { lineWidth: 1000})} \`\`\` `; } await writeFile(bicepReadmePath, generatedContent); }