import { InitializedByFlags, SdkCredentialParameter, SdkEndpointParameter, SdkHeaderParameter, SdkHttpParameter, SdkMethod, SdkMethodParameter, SdkModelPropertyType, SdkQueryParameter, SdkServiceMethod, SdkServiceOperation, SdkServiceResponseHeader, SdkType, } from "@azure-tools/typespec-client-generator-core"; import { getNamespaceFullName } from "@typespec/compiler"; import { marked, Token } from "marked"; import { PythonSdkContext } from "./lib.js"; import { getSimpleTypeResult, getType } from "./types.js"; function IsFullyUpperCase(identifier: string, maxUppercasePreserve: number) { const len = identifier.length; if (len > 1) { if (len <= maxUppercasePreserve && identifier === identifier.toUpperCase()) { return true; } if (len <= maxUppercasePreserve + 1 && identifier.endsWith("s")) { const i = identifier.substring(0, len - 1); if (i.toUpperCase() === i) { return true; } } } return false; } function deconstruct( identifier: string | Array, maxUppercasePreserve: number, ): Array { if (Array.isArray(identifier)) { return [...identifier.flatMap((each) => deconstruct(each, maxUppercasePreserve))]; } return `${identifier}` .replace(/([a-z]+)([A-Z])/g, "$1 $2") // Add a space in between camelCase words(e.g. fooBar => foo Bar) .replace(/(\d+)/g, " $1 ") // Adds a space after numbers(e.g. foo123 => foo123 bar) .replace(/\b([A-Z]+)([A-Z])s([^a-z])(.*)/g, "$1$2« $3$4") // Add a space after a plural upper cased word(e.g. MBsFoo => MBs Foo) .replace(/\b([A-Z]+)([A-Z])([a-z]+)/g, "$1 $2$3") // Add a space between an upper case word(2 char+) and the last capital case.(e.g. SQLConnection -> SQL Connection) .replace(/«/g, "s") .trim() .split(/[\W|_]+/) .map((each) => (IsFullyUpperCase(each, maxUppercasePreserve) ? each : each.toLowerCase())); } function isEqual(s1: string, s2: string): boolean { // when s2 is undefined and s1 is the string 'undefined', it returns 0, making this true. // To prevent that, first we need to check if s2 is undefined. return s2 !== undefined && !!s1 && !s1.localeCompare(s2, undefined, { sensitivity: "base" }); } function removeSequentialDuplicates(identifier: Iterable) { const ids = [...identifier].filter((each) => !!each); for (let i = 0; i < ids.length; i++) { while (isEqual(ids[i], ids[i - 1])) { ids.splice(i, 1); } while (isEqual(ids[i], ids[i - 2]) && isEqual(ids[i + 1], ids[i - 1])) { ids.splice(i, 2); } } return ids; } function normalize( identifier: string | Array, removeDuplicates = true, maxUppercasePreserve = 0, ): Array { if (!identifier || identifier.length === 0) { return [""]; } return typeof identifier === "string" ? normalize( deconstruct(identifier, maxUppercasePreserve), removeDuplicates, maxUppercasePreserve, ) : removeDuplicates ? removeSequentialDuplicates(identifier) : identifier; } // filter continuous empty strings at the beginning of the array function filterEmptyStrings(arr: string[]): string[] { const firstNonEmptyIndex = arr.findIndex((s) => s !== ""); return firstNonEmptyIndex >= 0 ? arr.slice(firstNonEmptyIndex) : arr; } export function camelToSnakeCase(name: string): string { if (!name) return name; const words = filterEmptyStrings(normalize(name, false, 6)); const result = words.join("_").toLowerCase(); const result_final = result.replace(/([^\d])_(\d+)/g, "$1$2"); return result_final; } export function getClientName(named: { name: string; isExactName: boolean }): string { return named.isExactName ? named.name : camelToSnakeCase(named.name); } export function getImplementation( context: PythonSdkContext, parameter: SdkEndpointParameter | SdkCredentialParameter | SdkMethodParameter | SdkHttpParameter, ): "Client" | "Method" { if (parameter.onClient) return "Client"; return "Method"; } export function isAbstract( method: SdkServiceMethod, ): boolean { return (method.operation.bodyParam?.contentTypes.length ?? 0) > 1 && method.access !== "internal"; } export function getDelimiterAndExplode( parameter: SdkQueryParameter | SdkHeaderParameter, ): [string | undefined, boolean] { if (parameter.type.kind !== "array") return [undefined, false]; let delimiter: string | undefined = undefined; let explode = parameter.kind === "query" && parameter.explode; if (parameter.collectionFormat === "csv" || parameter.collectionFormat === "simple") { delimiter = "comma"; } else if (parameter.collectionFormat === "ssv") { delimiter = "space"; } else if (parameter.collectionFormat === "tsv") { delimiter = "tab"; } else if (parameter.collectionFormat === "pipes") { delimiter = "pipe"; } else { explode = true; } return [delimiter, explode]; } type ParamBase = { optional: boolean; description: string; addedOn: string | undefined; clientName: string; isExactName: boolean; inOverload: boolean; isApiVersion: boolean; type: Record; isContinuationToken: boolean; apiVersions: string[]; }; export function getAddedOn( context: PythonSdkContext, type: | SdkEndpointParameter | SdkCredentialParameter | SdkModelPropertyType | SdkMethodParameter | SdkHttpParameter | SdkMethod, serviceApiVersions: string[] = [], ): string | undefined { // if type is added in the first version of the client, we do not need to add the versioning info const apiVersions = serviceApiVersions.length > 0 ? serviceApiVersions : (context.sdkPackage.clients.find( (c) => c.clientInitialization.initializedBy | InitializedByFlags.Individually, )?.apiVersions ?? []); if (type.apiVersions[0] === apiVersions[0]) return undefined; return type.apiVersions[0]; } export function isContinuationToken( parameter: SdkMethodParameter | SdkHttpParameter | SdkServiceResponseHeader, method?: SdkServiceMethod, input: boolean = true, ): boolean { const parameterSegments = method && method.kind === "paging" ? method.pagingMetadata.continuationTokenParameterSegments : undefined; const responseSegments = method && method.kind === "paging" ? method.pagingMetadata.continuationTokenResponseSegments : undefined; if (!parameterSegments || !responseSegments) return false; if (input) { return Boolean( parameterSegments && parameterSegments.length > 0 && (parameter.kind === "header" || parameter.kind === "query" || parameter.kind === "body") && parameterSegments.at(-1) === parameter.correspondingMethodParams.at(-1), ); } return Boolean( responseSegments && responseSegments.length > 0 && responseSegments.at(-1) === parameter, ); } export function emitParamBase( context: PythonSdkContext, parameter: SdkEndpointParameter | SdkCredentialParameter | SdkMethodParameter | SdkHttpParameter, method?: SdkServiceMethod, serviceApiVersions: string[] = [], ): ParamBase { let type = getType(context, parameter.type); if (parameter.isApiVersionParam) { if (parameter.clientDefaultValue) { type = getSimpleTypeResult(context, { type: "constant", value: parameter.clientDefaultValue, valueType: type, }); } } let clientName = getClientName(parameter); if ( parameter.kind !== "method" && parameter.kind !== "credential" && parameter.kind !== "endpoint" && parameter.onClient && parameter.correspondingMethodParams[0] ) { clientName = getClientName(parameter.correspondingMethodParams[0]); } return { optional: parameter.optional, description: (parameter.summary ? parameter.summary : parameter.doc) ?? "", addedOn: getAddedOn(context, parameter, serviceApiVersions), clientName, isExactName: parameter.isExactName, inOverload: false, isApiVersion: parameter.isApiVersionParam, isContinuationToken: parameter.kind !== "endpoint" && parameter.kind !== "credential" && isContinuationToken(parameter, method), type, apiVersions: parameter.apiVersions, }; } export function isAzureCoreErrorResponse(t: SdkType | undefined): boolean { if (!t) return false; const tspType = t.__raw; if (!tspType) return false; return ( tspType.kind === "Model" && tspType.namespace !== undefined && ["Azure.Core", "Azure.Core.Foundations"].includes(getNamespaceFullName(tspType.namespace)) && tspType.name === "ErrorResponse" ); } export function capitalize(name: string): string { return name[0].toUpperCase() + name.slice(1); } /** * Quotes a value so it can be safely embedded in a shell command line. * * The value is wrapped in double quotes (handling spaces and other separators) * and any embedded double quotes are escaped. This quoting must only be applied * when the value is passed through a shell (e.g. `execSync`); it must never be * baked into the option value itself, otherwise the quotes leak into non-shell * consumers such as the Pyodide runtime and end up in generated files. */ export function quoteShellArg(value: string): string { return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; } // Library namespaces that should not be used as client namespaces const LIB_NAMESPACE = [ "azure.core", "azure.resourcemanager", "azure.clientgenerator.core", "typespec.rest", "typespec.http", "typespec.versioning", ]; export function getRootNamespace(context: PythonSdkContext): string { if (context.sdkPackage.clients.length > 0) { return context.sdkPackage.clients[0].namespace.toLowerCase(); } else if (context.sdkPackage.namespaces.length > 0) { return context.sdkPackage.namespaces[0].fullName.toLowerCase(); } return ""; } function isLibraryNamespace(namespace: string): boolean { const ns = namespace.toLowerCase(); return LIB_NAMESPACE.some((lib) => ns.startsWith(lib)); } export function getClientNamespace(context: PythonSdkContext, clientNamespace: string) { // Namespace precedence: @clientNamespace > --namespace > original namespace // These are resolved by TCGC and passed in as clientNamespace. // However, models from library namespaces (azure.core, azure.resourcemanager, etc.) // should use the SDK's root namespace instead. if (clientNamespace === "" || isLibraryNamespace(clientNamespace)) { return getRootNamespace(context); } return clientNamespace.toLowerCase(); } function parseToken(token: Token): string { let parsed = ""; switch (token.type) { case "heading": parsed += `${"=".repeat(token.text.length)}\n${token.text}\n${"=".repeat( token.text.length, )}\n\n`; break; case "paragraph": parsed += `${token.text}\n\n`; break; case "strong": parsed += `**${token.text}**`; break; case "em": parsed += `*${token.text}*`; break; case "codespan": parsed += `\`\`${token.text}\`\``; break; case "code": { let codeBlockStyle = token.codeBlockStyle; if (codeBlockStyle === undefined) { codeBlockStyle = token.raw.split("\n")[0].replace("```", "").trim(); } // Convert invalid Pygments lexer names to valid ones if (codeBlockStyle === "txt") { codeBlockStyle = "text"; } parsed += `\n\n.. code-block:: ${codeBlockStyle ?? ""}\n\n ${token.text.split("\n").join("\n ")}`; break; } case "link": if (token.href !== undefined) { parsed += `\`${token.text} <${token.href}>\`_`; break; } parsed += `${token.text}`; break; case "list": if (!token.ordered) { parsed += `\n\n${token.items.map((item: any) => `* ${item.text}`).join("\n")}`; break; } parsed += `\n\n${token.items.map((item: any, index: number) => `${index + 1}. ${item.text}`).join("\n")}`; break; default: parsed += token.raw; } return parsed; } export function md2Rst(text?: string): string | undefined { try { if (!text || text === "") return text; const tokens = marked.lexer(text); let rst = ""; tokens.forEach((token: Token) => { if (token.type === "heading") { // Heading tokens are block level, so we should check if there are additional tokens inside const parsedHeadingText = md2Rst(token.text); rst += `${"=".repeat( parsedHeadingText!.length, )}\n${parsedHeadingText}\n${"=".repeat(parsedHeadingText!.length)}\n\n`; } else if ("tokens" in token && token.tokens !== undefined && token.tokens.length > 0) { token.tokens.forEach((element: any) => { rst += parseToken(element); }); } else { rst += parseToken(token); } }); // Trim trailing whitespace or tabs return rst.replace(/[ \t]+$/, ""); } catch (e) { if (e instanceof RangeError) { // The error is thrown by the tokenizer when the markdown is too long // We can ignore it and return the original text return text; } } return text; }