1+ // Copyright (c) Microsoft Corporation.
2+ // Licensed under the MIT License.
3+ import path from 'path' ;
4+ import os from 'os' ;
5+ import { findRecursive , executeCmd , fileExists } from './utils' ;
6+ import * as constants from './constants' ;
7+ import { readFile , writeFile } from 'fs/promises' ;
8+ import * as markdown from '@ts-common/commonmark-to-markdown'
9+ import * as yaml from 'js-yaml'
10+
11+ const autorestBinary = os . platform ( ) === 'win32' ? 'autorest.cmd' : 'autorest' ;
12+
13+ const rootDir = `${ __dirname } /../` ;
14+ const extensionDir = path . resolve ( `${ rootDir } /bicep-types-az/src/autorest.bicep/` ) ;
15+
16+ export async function generateAutorestV2Config ( readmePath : string , bicepReadmePath : string ) {
17+ // We expect a path format convention of <provider>/(any/number/of/intervening/folders)/<yyyy>-<mm>-<dd>(|-preview)/<filename>.json
18+ // This information is used to generate individual tags in the generated autorest configuration
19+ // eslint-disable-next-line no-useless-escape
20+ const pathRegex = / ^ ( \$ \( t h i s - f o l d e r \) \/ | ) ( [ ^ \/ ] + ) (?: \/ [ ^ \/ ] + ) * \/ ( \d { 4 } - \d { 2 } - \d { 2 } ( | - p r e v i e w ) ) \/ .* \. j s o n $ / i;
21+
22+ const readmeContents = await readFile ( readmePath , { encoding : 'utf8' } ) ;
23+ const readmeMarkdown = markdown . parse ( readmeContents ) ;
24+
25+ const inputFiles = new Set < string > ( ) ;
26+ // we need to look for all autorest configuration elements containing input files, and collect that list of files. These will look like (e.g.):
27+ // ```yaml $(tag) == 'someTag'
28+ // input-file:
29+ // - path/to/file.json
30+ // - path/to/other_file.json
31+ // ```
32+ for ( const node of markdown . iterate ( readmeMarkdown . markDown ) ) {
33+ // We're only interested in yaml code blocks
34+ if ( node . type !== 'code_block' || ! node . info || ! node . literal ||
35+ ! node . info . trim ( ) . startsWith ( 'yaml' ) ) {
36+ continue ;
37+ }
38+
39+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40+ const yamlData = yaml . load ( node . literal ) as any ;
41+ if ( yamlData ) {
42+ // input-file may be a single string or an array of strings
43+ const inputFile = yamlData [ 'input-file' ] ;
44+ if ( typeof inputFile === 'string' ) {
45+ inputFiles . add ( inputFile ) ;
46+ } else if ( inputFile instanceof Array ) {
47+ for ( const i of inputFile ) {
48+ inputFiles . add ( i ) ;
49+ }
50+ }
51+ }
52+ }
53+
54+ const filesByTag : Record < string , string [ ] > = { } ;
55+ for ( const file of inputFiles ) {
56+ const normalizedFile = normalizeJsonPath ( file ) ;
57+ const match = pathRegex . exec ( normalizedFile ) ;
58+ if ( match ) {
59+ // Generate a unique tag. We can't process all of the different API versions in one autorest pass
60+ // because there are constraints on naming uniqueness (e.g. naming of definitions), so we want to pass over
61+ // each API version separately.
62+ const tagName = `${ match [ 2 ] . toLowerCase ( ) } -${ match [ 3 ] . toLowerCase ( ) } ` ;
63+ if ( ! filesByTag [ tagName ] ) {
64+ filesByTag [ tagName ] = [ ] ;
65+ }
66+
67+ filesByTag [ tagName ] . push ( normalizedFile ) ;
68+ } else {
69+ console . warn ( `WARNING: Unable to parse swagger path "${ file } "` ) ;
70+ }
71+ }
72+
73+ let generatedContent = `##Bicep
74+
75+ ### Bicep multi-api
76+ \`\`\`yaml $(bicep) && $(multiapi)
77+ ${ yaml . dump ( { 'batch' : Object . keys ( filesByTag ) . map ( tag => ( { 'tag' : tag } ) ) } , { lineWidth : 1000 } ) }
78+ \`\`\`
79+ ` ;
80+
81+ for ( const tag of Object . keys ( filesByTag ) ) {
82+ generatedContent += `### Tag: ${ tag } and bicep
83+ \`\`\`yaml $(tag) == '${ tag } ' && $(bicep)
84+ ${ yaml . dump ( { 'input-file' : filesByTag [ tag ] } , { lineWidth : 1000 } ) }
85+ \`\`\`
86+ ` ;
87+ }
88+
89+ await writeFile ( bicepReadmePath , generatedContent ) ;
90+ }
91+
92+ function normalizeJsonPath ( jsonPath : string ) {
93+ // eslint-disable-next-line no-useless-escape
94+ return path . normalize ( jsonPath ) . replace ( / [ \\ \/ ] / g, '/' ) ;
95+ }
96+
97+ async function execAutoRest ( tmpFolder : string , params : string [ ] ) {
98+ await executeCmd ( __dirname , `${ __dirname } /node_modules/.bin/${ autorestBinary } ` , params ) ;
99+ if ( ! fileExists ( tmpFolder ) ) {
100+ return [ ] ;
101+ }
102+
103+ return await findRecursive ( tmpFolder , p => path . extname ( p ) === '.json' ) ;
104+ }
105+
106+ export async function runAutorestV2 ( readme : string , tmpFolder : string ) {
107+ const autoRestParams = [
108+ `--use=@autorest/modelerfour` ,
109+ `--use=${ extensionDir } ` ,
110+ '--bicep' ,
111+ `--output-folder=${ tmpFolder } ` ,
112+ '--multiapi' ,
113+ '--title=none' ,
114+ // This is necessary to avoid failures such as "ERROR: Semantic violation: Discriminator must be a required property." blocking type generation.
115+ // 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
116+ // 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.
117+ `--skip-semantics-validation` ,
118+ `--arm-schema=true` ,
119+ readme ,
120+ ] ;
121+
122+ if ( constants . autoRestVerboseOutput ) {
123+ autoRestParams . push ( '--verbose' ) ;
124+ }
125+
126+ return await execAutoRest ( tmpFolder , autoRestParams ) ;
127+ }
0 commit comments