Skip to content

clean up cadl code with tim #1657

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

Merged
merged 3 commits into from
Jan 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
1 change: 1 addition & 0 deletions packages/autorest.python/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"install": "node run-python3.js install.py",
"debug": "node run-python3.js start.py --debug"
},
"main": "index.js",
"repository": {
"type": "git",
"url": "https://github.com/Azure/autorest.python/tree/autorestv3"
Expand Down
8 changes: 4 additions & 4 deletions packages/cadl-python/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,20 @@
],
"peerDependencies": {
"@cadl-lang/versioning": "~0.38.0",
"@cadl-lang/rest": "~0.38.0",
"@cadl-lang/rest": "~0.38.1",
"@azure-tools/cadl-azure-core": "~0.24.0"
},
"dependencies": {
"js-yaml": "~4.1.0",
"@autorest/python": "workspace:^",
"@cadl-lang/compiler": "~0.38.0",
"@azure-tools/cadl-dpg": "~0.24.0"
"@cadl-lang/compiler": "~0.38.5",
"@azure-tools/cadl-dpg": "~0.25.0-dev.8"
},
"devDependencies": {
"@types/mocha": "~9.1.0",
"@types/node": "^18.7.16",
"@types/js-yaml": "~4.0.1",
"@cadl-lang/rest": "~0.38.0",
"@cadl-lang/rest": "~0.38.1",
"@cadl-lang/versioning": "~0.38.0",
"@cadl-lang/eslint-config-cadl": "~0.5.0",
"eslint": "^8.23.1",
Expand Down
130 changes: 59 additions & 71 deletions packages/cadl-python/src/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import {
ModelProperty,
Namespace,
Program,
resolvePath,
Type,
getEffectiveModelType,
JSONSchemaType,
createCadlLibrary,
Expand All @@ -41,6 +39,8 @@ import {
isNullType,
SyntaxKind,
emitFile,
Type,
getKnownValues,
} from "@cadl-lang/compiler";
import {
getAuthentication,
Expand All @@ -53,23 +53,29 @@ import {
HttpAuth,
HttpOperationParameter,
HttpOperationParameters,
HttpOperationRequestBody,
HttpOperationResponse,
HttpOperationResponseContent,
HttpServer,
isStatusCode,
} from "@cadl-lang/rest/http";
import { getAddedOn } from "@cadl-lang/versioning";
import { execFileSync } from "child_process";
import { dump } from "js-yaml";
import {
Client,
listClients,
listOperationGroups,
listOperationsInOperationGroup,
isApiVersion,
getDefaultApiVersion,
getClientNamespaceString,
createDpgContext,
DpgContext,
} from "@azure-tools/cadl-dpg";
import { getResourceOperation } from "@cadl-lang/rest";
import { execAsync, resolveModuleRoot, saveCodeModelAsYaml } from "./external-process.js";
import { dirname } from "path";
import { fileURLToPath } from "url";
import { dump } from "js-yaml";

interface HttpServerParameter {
type: "endpointPath";
Expand Down Expand Up @@ -129,13 +135,13 @@ export async function $onEmit(context: EmitContext<EmitterOptions>) {
const program = context.program;
const resolvedOptions = { ...defaultOptions, ...context.options };

const root = process.cwd();
const root = await resolveModuleRoot(program, "@autorest/python", dirname(fileURLToPath(import.meta.url)));
const outputDir = context.emitterOutputDir;
const yamlMap = createYamlEmitter(program);
const yamlPath = resolvePath(outputDir, "output.yaml");
const yamlMap = emitCodeModel(context);
const yamlPath = await saveCodeModelAsYaml("cadl-python-yaml-map", yamlMap);
const commandArgs = [
`${root}/node_modules/@autorest/python/run-python3.js`,
`${root}/node_modules/@autorest/python/run_cadl.py`,
`${root}/run-python3.js`,
`${root}/run_cadl.py`,
`--output-folder=${outputDir}`,
`--cadl-file=${yamlPath}`,
];
Expand All @@ -146,16 +152,7 @@ export async function $onEmit(context: EmitContext<EmitterOptions>) {
commandArgs.push("--debug");
}
if (!program.compilerOptions.noEmit && !program.hasError()) {
// TODO: change behavior based off of https://github.com/microsoft/cadl/issues/401
await emitFile(program, {
path: yamlPath,
content: dump(yamlMap),
newLine: "lf",
});
execFileSync(process.execPath, commandArgs);
}
if (program.compilerOptions.trace === undefined) {
await program.host.rm(yamlPath);
await execAsync(process.execPath, commandArgs);
}
}

Expand Down Expand Up @@ -288,7 +285,14 @@ function getAddedOnVersion(p: Program, t: Type): string | undefined {
return getAddedOn(p as any, t as any)?.value;
}

function emitParamBase(program: Program, parameter: ModelProperty | Type): Record<string, any> {
type ParamBase = {
optional: boolean;
description: string;
addedOn: string | undefined;
clientName: string;
inOverload: boolean;
};
function emitParamBase(program: Program, parameter: ModelProperty | Type): ParamBase {
let optional: boolean;
let name: string;
let description: string = "";
Expand All @@ -313,28 +317,29 @@ function emitParamBase(program: Program, parameter: ModelProperty | Type): Recor
};
}

function emitBodyParameter(
program: Program,
bodyType: Type,
params: HttpOperationParameters,
operation: Operation,
): Record<string, any> {
const base = emitParamBase(program, params.bodyParameter ?? bodyType);
const contentTypeParam = params.parameters.find((p) => p.type === "header" && p.name === "content-type");
const contentTypes = contentTypeParam
? ignoreDiagnostics(getContentTypes(contentTypeParam.param))
: ["application/json"];
type BodyParameter = ParamBase & {
contentTypes: string[];
type: Type;
restApiName: string;
location: "body";
defaultContentType: string;
};

function emitBodyParameter(program: Program, body: HttpOperationRequestBody, operation: Operation): BodyParameter {
const base = emitParamBase(program, body.parameter ?? body.type);
let contentTypes = body.contentTypes;
if (contentTypes.length === 0) {
contentTypes = ["application/json"];
}
if (contentTypes.length !== 1) {
throw Error("Currently only one kind of content-type!");
}
let type;
const resourceOperation = getResourceOperation(program, operation);
if (resourceOperation) {
type = getType(program, resourceOperation.resourceType);
} else if (params.body) {
type = getType(program, params.body.type);
} else {
type = getType(program, bodyType);
type = getType(program, body.type);
}

if (type.type === "model" && type.name === "") {
Expand All @@ -344,10 +349,11 @@ function emitBodyParameter(
return {
contentTypes,
type,
restApiName: params.bodyParameter?.name ?? "body",
restApiName: body.parameter?.name ?? "body",
location: "body",
...base,
defaultContentType: contentTypes.includes("application/json") ? "application/json" : contentTypes[0],
defaultContentType:
body.parameter?.default ?? contentTypes.includes("application/json") ? "application/json" : contentTypes[0],
};
}

Expand Down Expand Up @@ -629,12 +635,7 @@ function emitBasicOperation(program: Program, operation: Operation, operationGro
if (httpOperation.parameters.body === undefined) {
bodyParameter = undefined;
} else {
bodyParameter = emitBodyParameter(
program,
httpOperation.parameters.body.type,
httpOperation.parameters,
operation,
);
bodyParameter = emitBodyParameter(program, httpOperation.parameters.body, operation);
if (parameters.filter((e) => e.restApiName.toLowerCase() === "content-type").length === 0) {
parameters.push(emitContentTypeParameter(bodyParameter, isOverload, isOverriden));
}
Expand Down Expand Up @@ -712,17 +713,6 @@ function getName(program: Program, type: Model): string {
}

function emitModel(program: Program, type: Model): Record<string, any> {
for (const decorator of type.decorators) {
if (decorator.decorator.name === "$knownValues") {
for (const arg of decorator.args) {
if (typeof arg.value === "object" && arg.value.kind === "Enum") {
const enumResult = emitEnum(program, arg.value);
enumResult["name"] = type.name;
return enumResult;
}
}
}
}
// Now we know it's a defined model
const properties: Record<string, any>[] = [];
let baseModel = undefined;
Expand Down Expand Up @@ -1217,11 +1207,12 @@ function getApiVersionParameter(program: Program): Record<string, any> | void {
}
}

function emitClients(program: Program, namespace: string): Record<string, any>[] {
function emitClients(context: DpgContext, namespace: string): Record<string, any>[] {
const program = context.program;
const clients = listClients(program);
const retval: Record<string, any>[] = [];
for (const client of clients) {
if (getNamespace(program, client.name) !== namespace) {
if (getNamespace(context, client.name) !== namespace) {
continue;
}
const server = getServerHelper(program, client.service);
Expand All @@ -1246,39 +1237,36 @@ function getServiceNamespace(program: Program): Namespace {
return listServices(program)[0].type;
}

function getServiceNamespaceString(program: Program): string {
return getNamespaceFullName(getServiceNamespace(program)).toLowerCase();
}

function getNamespace(program: Program, clientName: string): string {
function getNamespace(context: DpgContext, clientName: string): string {
// We get client namespaces from the client name. If there's a dot, we add that to the namespace
const submodule = clientName.split(".").slice(0, -1).join(".").toLowerCase();
if (!submodule) {
return getServiceNamespaceString(program).toLowerCase();
return getClientNamespaceString(context)!.toLowerCase();
}
return submodule;
}

function getNamespaces(program: Program): Set<string> {
function getNamespaces(context: DpgContext): Set<string> {
const namespaces = new Set<string>();
for (const client of listClients(program)) {
namespaces.add(getNamespace(program, client.name));
for (const client of listClients(context.program)) {
namespaces.add(getNamespace(context, client.name));
}
return namespaces;
}

function createYamlEmitter(program: Program) {
const serviceNamespaceString = getServiceNamespaceString(program);
function emitCodeModel(context: EmitContext<EmitterOptions>) {
const dpgContext = createDpgContext(context);
const clientNamespaceString = getClientNamespaceString(dpgContext);
// Get types
const codeModel: Record<string, any> = {
namespace: serviceNamespaceString,
namespace: clientNamespaceString,
subnamespaceToClients: {},
};
for (const namespace of getNamespaces(program)) {
if (namespace === serviceNamespaceString) {
codeModel["clients"] = emitClients(program, namespace);
for (const namespace of getNamespaces(dpgContext)) {
if (namespace === clientNamespaceString) {
codeModel["clients"] = emitClients(dpgContext, namespace);
} else {
codeModel["subnamespaceToClients"][namespace] = emitClients(program, namespace);
codeModel["subnamespaceToClients"][namespace] = emitClients(dpgContext, namespace);
}
}
codeModel["types"] = [...typesMap.values(), ...Object.values(KnownTypes), ...simpleTypesMap.values()];
Expand Down
80 changes: 80 additions & 0 deletions packages/cadl-python/src/external-process.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { CompilerHost, joinPaths, Program, resolveModule, ResolveModuleHost } from "@cadl-lang/compiler";
import { ChildProcess, spawn, SpawnOptions } from "child_process";
import { randomUUID } from "crypto";
import { mkdir, writeFile } from "fs/promises";
import jsyaml from "js-yaml";
import os from "os";

const cadlCodeGenTempDir = joinPaths(os.tmpdir(), "cadl-codegen");

export function createTempPath(extension: string, prefix: string = "") {
return joinPaths(cadlCodeGenTempDir, prefix + randomUUID() + extension);
}

/**
* Save the given codemodel in a yaml file.
* @param name Name of the codemodel. To give a guide to the temp file name.
* @param codemodel Codemodel to save
* @return the absolute path to the created codemodel.
*/
export async function saveCodeModelAsYaml(name: string, codemodel: unknown): Promise<string> {
await mkdir(cadlCodeGenTempDir, { recursive: true });
const filename = createTempPath(".yaml", name);
const yamlStr = jsyaml.dump(codemodel);
await writeFile(filename, yamlStr);
return filename;
}

/**
* Start external process async
* @param command Command to run. This is the just the executable path or name.
* @param args Command arguments.
* @param options Options
*/
export async function execAsync(
command: string,
args: string[],
options: SpawnOptions = {},
): Promise<{ exitCode: number; proc: ChildProcess }> {
const child = spawn(command, args, { stdio: "inherit", ...options });
return new Promise((resolve, reject) => {
child.on("error", (error) => {
reject(error);
});

child.on("exit", (exitCode) => {
resolve({
exitCode: exitCode ?? -1,
proc: child,
});
});
});
}

/**
* Resolve root of module.
* @param program Cadl Program
* @param name Name of the module (e.g. "@autorest/python")
* @param baseDir Base directory to start looking from. Use `dirname(fileURLToPath(import.meta.url))` for current dir.
* @returns Path to the module root if found.
*/
export async function resolveModuleRoot(program: Program, name: string, baseDir: string): Promise<string> {
const moduleHost = getResolveModuleHost(program.host);

const resolved = await resolveModule(moduleHost, name, {
baseDir,
});

return resolved.path;
}

function getResolveModuleHost(host: CompilerHost): ResolveModuleHost {
return {
realpath: host.realpath,
stat: host.stat,
readFile: async (path) => {
const file = await host.readFile(path);
return file.text;
},
};
}
4 changes: 2 additions & 2 deletions packages/cadl-python/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"composite": true,
"alwaysStrict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"module": "Node16",
"moduleResolution": "Node16",
"esModuleInterop": true,
"noImplicitAny": true,
"noImplicitReturns": true,
Expand Down
Loading