-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7eb9fcf
commit 9da2716
Showing
12 changed files
with
586 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import openapitools from '../openapitools.json'; | ||
import fsp from 'fs/promises'; | ||
import path from 'path'; | ||
|
||
export const availableLanguages = ['javascript'] as const; | ||
export type Language = typeof availableLanguages[number]; | ||
|
||
function printUsage(commandName: string): void { | ||
console.log(`usage: ${commandName} all | language1 language2...`); | ||
console.log(`\tavailable languages: ${availableLanguages.join(',')}`); | ||
// eslint-disable-next-line no-process-exit | ||
process.exit(1); | ||
} | ||
|
||
export async function* walk( | ||
dir: string | ||
): AsyncGenerator<{ path: string; name: string }> { | ||
for await (const d of await fsp.opendir(dir)) { | ||
const entry = path.join(dir, d.name); | ||
if (d.isDirectory()) yield* walk(entry); | ||
else if (d.isFile()) yield { path: entry, name: d.name }; | ||
} | ||
} | ||
|
||
export const extensionForLanguage: Record<Language, string> = { | ||
javascript: '.test.ts', | ||
}; | ||
|
||
// For each generator, we map the packageName with the language and client | ||
export const packageNames: Record< | ||
string, | ||
Record<Language, string> | ||
> = Object.entries(openapitools['generator-cli'].generators).reduce( | ||
(prev, [clientName, clientConfig]) => { | ||
const obj = prev; | ||
const parts = clientName.split('-'); | ||
const lang = parts[0] as Language; | ||
const client = parts.slice(1).join('-'); | ||
|
||
if (!(lang in prev)) { | ||
obj[lang] = {}; | ||
} | ||
|
||
obj[lang][client] = clientConfig.additionalProperties.packageName; | ||
|
||
return obj; | ||
}, | ||
{} as Record<string, Record<string, string>> | ||
); | ||
|
||
function capitalize(str: string): string { | ||
return str.charAt(0).toUpperCase() + str.slice(1); | ||
} | ||
|
||
export function createClientName(client: string): string { | ||
return `${client | ||
.split('-') | ||
.map((part) => capitalize(part)) | ||
.join('')}Api`; | ||
} | ||
|
||
export async function parseCLI( | ||
args: string[], | ||
commandName: string | ||
): Promise<Language[]> { | ||
if (args.length < 3) { | ||
console.log('not enough arguments'); | ||
printUsage(commandName); | ||
} | ||
|
||
let toGenerate: Language[]; | ||
if (args.length === 3 && args[2] === 'all') { | ||
toGenerate = [...availableLanguages]; | ||
} else { | ||
const languages = args[2].split(' ') as Language[]; | ||
const unknownLanguages = languages.filter( | ||
(lang) => !availableLanguages.includes(lang) | ||
); | ||
if (unknownLanguages.length > 0) { | ||
console.log('unkown language(s): ', unknownLanguages.join(', ')); | ||
printUsage(commandName); | ||
} | ||
toGenerate = languages; | ||
} | ||
|
||
return toGenerate; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
/* eslint-disable no-console */ | ||
import fsp from 'fs/promises'; | ||
import Mustache from 'Mustache'; | ||
import { | ||
packageNames, | ||
parseCLI, | ||
walk, | ||
createClientName, | ||
extensionForLanguage, | ||
type Language, | ||
} from './common'; | ||
|
||
type TestCase = { | ||
testName: string; | ||
autoCreateClient?: boolean; // `true` by default | ||
autoCreateIndex?: boolean; // `false` by default | ||
steps: Step[]; | ||
}; | ||
|
||
type Step = CreateClientStep | VariableStep | MethodStep; | ||
|
||
type CreateClientStep = { | ||
type: 'createClient'; | ||
parameters: { | ||
appId: string; | ||
apiKey: string; | ||
}; | ||
expected?: Expected; | ||
}; | ||
|
||
type VariableStep = { | ||
type: 'variable'; | ||
object: string; | ||
path: string[]; | ||
expected?: Expected; | ||
}; | ||
|
||
type MethodStep = { | ||
type: 'method'; | ||
object: string; | ||
path: string[]; | ||
parameters?: any; | ||
expected?: Expected; | ||
}; | ||
|
||
type Expected = { | ||
length?: number; | ||
error?: string; | ||
match?: { objectContaining: object } | any; | ||
}; | ||
|
||
type IntegrationTestsBlock = { | ||
operationId: string; | ||
tests: TestCase[]; | ||
}; | ||
|
||
type AllTests = { | ||
[client: string]: IntegrationTestsBlock[]; | ||
}; | ||
|
||
async function loadIntegrationTests() { | ||
const integrationTests: AllTests = {}; | ||
for await (const { name: client } of await fsp.opendir( | ||
'./integration/clients/' | ||
)) { | ||
integrationTests[client] = await loadIntegrationTestsForClient(client); | ||
} | ||
return integrationTests; | ||
} | ||
|
||
async function loadIntegrationTestsForClient(client: string) { | ||
const testBlocks: IntegrationTestsBlock[] = []; | ||
for await (const file of walk(`./integration/clients/${client}`)) { | ||
if (!file.name.endsWith('.json')) { | ||
continue; | ||
} | ||
const fileName = file.name.replace('.json', ''); | ||
const fileContent = (await fsp.readFile(file.path)).toString(); | ||
|
||
if (!fileContent) { | ||
throw new Error(`cannot read empty file ${fileName} - ${client} client`); | ||
} | ||
|
||
const tests: TestCase[] = JSON.parse(fileContent).map((testCase) => { | ||
if (!testCase.testName) { | ||
throw new Error( | ||
`Cannot have a test with no name ${fileName} - ${client} client` | ||
); | ||
} | ||
return { | ||
autoCreateClient: true, | ||
autoCreateIndex: false, | ||
...testCase, | ||
}; | ||
}); | ||
|
||
testBlocks.push({ | ||
operationId: fileName, | ||
tests, | ||
}); | ||
} | ||
|
||
return testBlocks; | ||
} | ||
|
||
async function loadTemplates(language: Language) { | ||
const templates: Record<string, string> = {}; | ||
for await (const file of walk(`./integration/templates/${language}`)) { | ||
if (!file.name.endsWith('.mustache')) { | ||
continue; | ||
} | ||
const type = file.name.replace('.mustache', ''); | ||
const fileContent = (await fsp.readFile(file.path)).toString(); | ||
templates[type] = fileContent; | ||
} | ||
return templates; | ||
} | ||
|
||
async function generateCode(language: Language, allClientsTests: AllTests) { | ||
await fsp.mkdir(`output/${language}`, { recursive: true }); | ||
for (const client in allClientsTests) { | ||
const { suite: template, ...partialTemplates } = await loadTemplates( | ||
language | ||
); | ||
const code = Mustache.render( | ||
template, | ||
{ | ||
import: packageNames[language][client], | ||
client: createClientName(client), | ||
blocks: modifyForMustache(allClientsTests[client]), | ||
}, | ||
partialTemplates | ||
); | ||
await fsp.writeFile( | ||
`output/${language}/${client}.integration${extensionForLanguage[language]}`, | ||
code | ||
); | ||
} | ||
} | ||
|
||
function modifyForMustache(blocks: IntegrationTestsBlock[]) { | ||
return blocks.map(({ tests, ...rest }) => ({ | ||
...rest, | ||
tests: tests.map(({ steps, ...rest }) => ({ | ||
...rest, | ||
steps: steps.map((step) => { | ||
const modified = { | ||
...step, | ||
isCreateClient: step.type === 'createClient', | ||
isVariable: step.type === 'variable', | ||
isMethod: step.type === 'method', | ||
}; | ||
|
||
if (step.type === 'method') { | ||
if (step.parameters) { | ||
let serialized = JSON.stringify(step.parameters); | ||
serialized = serialized.slice(1, serialized.length - 1); | ||
// @ts-expect-error | ||
modified.parameters = serialized; | ||
} | ||
} | ||
|
||
if (step.expected && step.expected.error) { | ||
// @ts-expect-error | ||
modified.expectedError = step.expected.error; | ||
} | ||
|
||
return modified; | ||
}), | ||
})), | ||
})); | ||
} | ||
|
||
parseCLI(process.argv, 'generateIntegrationTests').then( | ||
async (languagesToGenerate) => { | ||
try { | ||
const allClientsTests = await loadIntegrationTests(); | ||
for (const language of languagesToGenerate) { | ||
generateCode(language, allClientsTests); | ||
} | ||
} catch (e) { | ||
if (e instanceof Error) { | ||
console.error(e); | ||
} | ||
} | ||
} | ||
); |
Oops, something went wrong.