Feature: Autorest display help from schema (#4323)

This commit is contained in:
Timothee Guerin 2021-10-04 13:16:45 -07:00 коммит произвёл GitHub
Родитель 3416054989
Коммит 85a7d20789
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
19 изменённых файлов: 696 добавлений и 386 удалений

Просмотреть файл

@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@autorest/common",
"comment": "**Fix** Issue with coloring [] wrapped elements",
"type": "patch"
}
],
"packageName": "@autorest/common",
"email": "tiguerin@microsoft.com"
}

Просмотреть файл

@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@autorest/configuration",
"comment": "**Consolidate** configuration schema to add description and missing settings",
"type": "minor"
}
],
"packageName": "@autorest/configuration",
"email": "tiguerin@microsoft.com"
}

Просмотреть файл

@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@autorest/core",
"comment": "**Updated** `--help` to use configuration schema and be consistent",
"type": "minor"
}
],
"packageName": "@autorest/core",
"email": "tiguerin@microsoft.com"
}

Просмотреть файл

@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "autorest",
"comment": "**Remove** legacy code",
"type": "minor"
}
],
"packageName": "autorest",
"email": "tiguerin@microsoft.com"
}

Просмотреть файл

@ -1,47 +1,3 @@
#!/usr/bin/env node
/* eslint-disable no-console */
global.isDebuggerEnabled =
!!require("inspector").url() || global.v8debug || /--debug|--inspect/.test(process.execArgv.join(" "));
const maxMemorySizeArg = process.argv.join(" ").match(/--max-memory-size=(\w+)/);
const maxMemorySize = maxMemorySizeArg && parseInt(maxMemorySizeArg[1]);
if (isNaN(maxMemorySize)) {
console.error(`\nWarning: --max-memory-size parameter '${maxMemorySizeArg[1]}' is not an integer, ignoring it.\n`);
}
// if the process was started with a low heap size (and we're not debugging!) then respawn with a bigger heap size.
if (
maxMemorySize &&
!isDebuggerEnabled &&
require("v8").getHeapStatistics().heap_size_limit < maxMemorySize * 1024000
) {
process.env["NODE_OPTIONS"] = `${
process.env["NODE_OPTIONS"] || ""
} --max-old-space-size=${maxMemorySize} --no-warnings`;
const argv =
process.argv.indexOf("--break") === -1
? process.argv.slice(1)
: ["--inspect-brk", ...process.argv.slice(1).filter((each) => each !== "--break")];
require("child_process")
.spawn(process.execPath, argv, { argv0: "node", stdio: "inherit" })
.on("close", (code) => {
process.exit(code);
});
} else {
try {
const v = process.versions.node.split(".");
if (v[0] < 12) {
console.error("\nFATAL: Node v12 or higher (v12.x minimum, v14.x LTS recommended) is required for AutoRest.\n");
process.exit(1);
}
if (v[0] > 14) {
console.error("\nWARNING: AutoRest has not been tested with Node versions greater than v14.\n");
}
require(`${__dirname}/../dist/app.js`);
} catch (e) {
console.error(e);
}
}
// Need to have this intermediate file due to rush issue with generated bin https://github.com/microsoft/rushstack/issues/2400
require("../dist/app.js");

Просмотреть файл

@ -6,7 +6,6 @@
/* eslint-disable no-console */
import "source-map-support/register";
declare const isDebuggerEnabled: boolean;
const cwd = process.cwd();
import chalk from "chalk";
@ -17,6 +16,9 @@ import { resetAutorest, showAvailableCoreVersions, showInstalledExtensions } fro
import { VERSION } from "./constants";
import { loadConfig, resolveCoreVersion } from "./utils";
const isDebuggerEnabled =
// eslint-disable-next-line node/no-unsupported-features/node-builtins
!!require("inspector").url() || global.v8debug || /--debug|--inspect/.test(process.execArgv.join(" "));
const launchCore = isDebuggerEnabled ? runCoreWithRequire : runCoreOutOfProc;
// aliases, round one.

Просмотреть файл

@ -34,10 +34,9 @@ import {
writeBinary,
writeString,
} from "@azure-tools/uri";
import { parseYAML } from "@azure-tools/yaml";
import { omit } from "lodash";
import { ArtifactWriter } from "./artifact-writer";
import { Help } from "./help";
import { printAutorestHelp } from "./commands";
import { Artifact } from "./lib/artifact";
import { AutoRest, IsOpenApiDocument, Shutdown } from "./lib/autorest-core";
import { VERSION } from "./lib/constants";
@ -211,45 +210,7 @@ async function currentMain(autorestArgs: Array<string>): Promise<number> {
}
if (context.config.help) {
// no fs operations on --help! Instead, format and print artifacts to console.
// - print boilerplate help
console.log("");
console.log("");
console.log(color("**Usage**: autorest `[configuration-file.md] [...options]`"));
console.log("");
console.log(color(" See: https://aka.ms/autorest/cli for additional documentation"));
// - sort artifacts by name (then content, just for stability)
const helpArtifacts = artifacts.sort((a, b) =>
a.uri === b.uri ? (a.content > b.content ? 1 : -1) : a.uri > b.uri ? 1 : -1,
);
// - format and print
for (const helpArtifact of helpArtifacts) {
const { result: help, errors } = parseYAML<Help>(helpArtifact.content);
if (errors.length > 0) {
for (const { message, position } of errors) {
console.error(color(`!Parsing error at **${helpArtifact.uri}**:__${position}: ${message}__`));
}
}
if (!help) {
continue;
}
const activatedBySuffix = help.activationScope ? ` (activated by --${help.activationScope})` : "";
console.log("");
console.log(color(`### ${help.categoryFriendlyName}${activatedBySuffix}`));
if (help.description) {
console.log(color(help.description));
}
console.log("");
for (const settingHelp of help.settings) {
const keyPart = `--${settingHelp.key}`;
const typePart = settingHelp.type ? `=<${settingHelp.type}>` : " "; // `[=<boolean>]`;
const settingPart = `${keyPart}\`${typePart}\``;
// if (!settingHelp.required) {
// settingPart = `[${settingPart}]`;
// }
console.log(color(` ${settingPart.padEnd(30)} **${settingHelp.description}**`));
}
}
printAutorestHelp(artifacts);
} else {
// perform file system operations.
await doClearFolders(protectFiles, clearFolders);

Просмотреть файл

@ -0,0 +1,154 @@
/* eslint-disable no-console */
import { color } from "@autorest/common";
import {
AUTOREST_CONFIGURATION_DEFINITION_FOR_HELP,
ConfigurationPropertyType,
ConfigurationSchemaDefinition,
RootConfigurationProperty,
} from "@autorest/configuration";
import { parseYAML } from "@azure-tools/yaml";
import { Artifact } from "../lib/artifact";
export function printAutorestHelp(artifacts: Artifact[]) {
printHelpHeader();
printHelpForConfigurationSchema(AUTOREST_CONFIGURATION_DEFINITION_FOR_HELP);
printHelpFromHelpContent(artifacts);
}
function printHelpHeader() {
console.log(
[
"",
"",
color("**Usage**: autorest `[configuration-file.md] with [...options]`"),
"",
color(" See: https://aka.ms/autorest/cli for additional documentation"),
].join("\n"),
);
}
interface Category {
name: string;
description?: string;
options: Option[];
}
interface Option {
key: string;
description: string;
type: ConfigurationPropertyType;
}
function groupFlagsByCategory(def: ConfigurationSchemaDefinition<any, any>): Category[] {
const categories = new Map<string, Category>();
const unknownCategories: Option[] = [];
for (const [key, { name, description }] of Object.entries(def.categories)) {
categories.set(key, { name, description, options: [] });
}
for (const [key, option] of Object.entries<RootConfigurationProperty<any>>(def.schema)) {
const category = categories.get(option.category);
const options = category === undefined ? unknownCategories : category.options;
options.push({
key,
description: option.description ?? "",
type: option.type,
});
}
return [...categories.values()];
}
function printHelpForConfigurationSchema(def: ConfigurationSchemaDefinition<any, any>) {
const categories = groupFlagsByCategory(def);
let maxKeyAndTypeLength = 0;
const processed = categories.map((category) => {
return {
...category,
options: category.options.map((option) => {
const keyPart = `--${option.key}`;
const typePart = printConfiguarationPropertyType(option.type);
const keyAndType = `${keyPart}\`${typePart}\``;
if (keyAndType.length > maxKeyAndTypeLength) {
maxKeyAndTypeLength = keyAndType.length;
}
return { keyAndType, description: option.description };
}),
};
});
for (const category of processed) {
console.log("");
console.log(color(`### ${category.name}`));
if (category.description) {
console.log(color(category.description));
}
for (const option of category.options) {
console.log(color(` ${option.keyAndType.padEnd(maxKeyAndTypeLength + 1)} **${option.description}**`));
}
}
}
function printConfiguarationPropertyType(type: ConfigurationPropertyType): string {
switch (type) {
case "boolean":
return " ";
default:
return `=<${type}>`;
}
}
export interface Help {
categoryFriendlyName: string; // e.g. "Output Verbosity", "C# generator"
activationScope?: string; // e.g. "csharp"
description?: string; // inline markdown allowed
settings: Array<SettingHelp>;
}
export interface SettingHelp {
required?: boolean; // defaults to "false"
key: string; // e.g. "namespace"
type?: string; // not specified => flag; otherwise, please use TypeScript syntax
description: string; // inline markdown allowed
}
/**
* Print help defined in an extension `help-content` section in the configuration.
*/
function printHelpFromHelpContent(artifacts: Artifact[]) {
// - sort artifacts by name (then content, just for stability)
const helpArtifacts = artifacts.sort((a, b) =>
a.uri === b.uri ? (a.content > b.content ? 1 : -1) : a.uri > b.uri ? 1 : -1,
);
// - format and print
for (const helpArtifact of helpArtifacts) {
const { result: help, errors } = parseYAML<Help>(helpArtifact.content);
if (errors.length > 0) {
for (const { message, position } of errors) {
console.error(color(`!Parsing error at **${helpArtifact.uri}**:__${position}: ${message}__`));
}
}
if (!help) {
continue;
}
const activatedBySuffix = help.activationScope ? ` (activated by --${help.activationScope})` : "";
console.log("");
console.log(color(`### ${help.categoryFriendlyName}${activatedBySuffix}`));
if (help.description) {
console.log(color(help.description));
}
console.log("");
for (const settingHelp of help.settings) {
const keyPart = `--${settingHelp.key}`;
const typePart = settingHelp.type ? `=<${settingHelp.type}>` : " "; // `[=<boolean>]`;
const settingPart = `${keyPart}\`${typePart}\``;
// if (!settingHelp.required) {
// settingPart = `[${settingPart}]`;
// }
console.log(color(` ${settingPart.padEnd(30)} **${settingHelp.description}**`));
}
}
}

Просмотреть файл

@ -0,0 +1 @@
export * from "./help";

Просмотреть файл

@ -1,13 +0,0 @@
export interface Help {
categoryFriendlyName: string; // e.g. "Output Verbosity", "C# generator"
activationScope?: string; // e.g. "csharp"
description?: string; // inline markdown allowed
settings: Array<SettingHelp>;
}
export interface SettingHelp {
required?: boolean; // defaults to "false"
key: string; // e.g. "namespace"
type?: string; // not specified => flag; otherwise, please use TypeScript syntax
description: string; // inline markdown allowed
}

Просмотреть файл

@ -4,13 +4,12 @@
*--------------------------------------------------------------------------------------------*/
import { QuickDataSource } from "@azure-tools/datastore";
import { Help } from "../../help";
import { PipelinePlugin } from "../pipeline/common";
/* @internal */
export function createHelpPlugin(): PipelinePlugin {
return async (config) => {
const help: { [helpKey: string]: Help } = config.GetEntry(<any>"help-content");
const help: { [helpKey: string]: any } = config.GetEntry(<any>"help-content");
for (const helpKey of Object.keys(help).sort()) {
config.GeneratedFile.Dispatch({
type: "help",

Просмотреть файл

@ -1,7 +1,8 @@
import { ConsoleLogger } from "@autorest/common";
import { AutorestConfiguration } from "@autorest/configuration";
import { createMockLogger } from "@autorest/test-utils";
import { MemoryFileSystem } from "@azure-tools/datastore";
import * as AutoRest from "../src/lib/autorest-core";
import { createMockLogger } from "@autorest/test-utils";
describe("Configuration", () => {
it("Test config", async () => {
@ -49,7 +50,11 @@ csharp:
]),
);
const autorest = new AutoRest.AutoRest(createMockLogger(), f, MemoryFileSystem.DefaultVirtualRootUri + "readme.md");
const autorest = new AutoRest.AutoRest(
new ConsoleLogger(),
f,
MemoryFileSystem.DefaultVirtualRootUri + "readme.md",
);
const context = await autorest.view;
const cfg = context.config;

Просмотреть файл

@ -43,4 +43,16 @@ describe("Coloring", () => {
it("gray ` quoted text", () => {
expect(color("some `backtick quote text`")).toEqual(`some ${chalk.gray("backtick quote text")}`);
});
it("color [] wrapped text", () => {
expect(color("some [arg] with [option]")).toEqual(
`some ${chalk.yellow.bold("[arg]")} with ${chalk.yellow.bold("[option]")}`,
);
});
it("color [] wrapped text with bold elements", () => {
expect(color("**some** [arg] with [option]")).toEqual(
`${chalk.bold("some")} ${chalk.yellow.bold("[arg]")} with ${chalk.yellow.bold("[option]")}`,
);
});
});

Просмотреть файл

@ -4,8 +4,8 @@ const NOT_EMPTY_LINE_REGEXP = /^(?!$)/gm;
export function color(text: string): string {
return text
.replace(/(\[.*?\])/gm, (_, x) => chalk.yellow.bold(x))
.replace(/\*\*(.*?)\*\*/gm, (_, x) => chalk.bold(x))
.replace(/(\[.*?s\])/gm, (_, x) => chalk.yellow.bold(x))
.replace(/^# (.*)/gm, (_, x) => chalk.greenBright(x))
.replace(/^## (.*)/gm, (_, x) => chalk.green(x))
.replace(/^### (.*)/gm, (_, x) => chalk.cyanBright(x))

Просмотреть файл

@ -13,118 +13,6 @@ pipeline:
output-artifact:
- help
help-content: # type: Help as defined in autorest-core/help.ts
_autorest-0:
categoryFriendlyName: Overall Verbosity
settings:
# - key: quiet
# description: suppress most output information
- key: verbose
description: display verbose logging information
- key: debug
description: display debug logging information
_autorest-1:
categoryFriendlyName: Manage Installation
settings:
- key: info # list-installed
description: display information about the installed version of autorest and its extensions
- key: list-available
description: display available AutoRest versions
- key: reset
description: removes all autorest extensions and downloads the latest version of the autorest-core extension
- key: preview
description: enables using autorest extensions that are not yet released
- key: latest
description: installs the latest autorest-core extension
- key: force
description: force the re-installation of the autorest-core extension and frameworks
- key: version
description: use the specified version of the autorest-core extension
type: string
- key: use
description: "Specify an extension to load and use. Format: `--use=<packagename>[@<version>]` (e.g. `--use=@autorest/modelerfour@~4.19.0`)"
type: string
_autorest-core-0:
categoryFriendlyName: Core Settings and Switches
settings:
- key: help
description: display help (combine with flags like --csharp to get further details about specific functionality)
- key: input-file
type: string | string[]
description: OpenAPI file to use as input (use this setting repeatedly to pass multiple files at once)
- key: output-folder
type: string
description: 'target folder for generated artifacts; default: "<base folder>/generated"'
- key: clear-output-folder
description: clear the output folder before writing generated artifacts to disk (use with extreme caution!)
- key: base-folder
type: string
description: "path to resolve relative paths (input/output files/folders) against; default: directory of configuration file, current directory otherwise"
- key: eol
type: '"default" | "lf" | "crlf"'
description: "Configure the line endings for generated files. Options: `default`, `lf`, `crlf`"
- key: message-format
type: '"regular" | "json"'
description: 'format of messages (e.g. from OpenAPI validation); default: "regular"'
- key: github-auth-token
type: string
description: OAuth token to use when pointing AutoRest at files living in a private GitHub repository
- key: azure-arm
description: Generate code in Azure flavor.
type: boolean
- key: head-as-boolean
description: When `true`, HEAD calls to non-existent resources (404) will not raise an error. Instead, if the resource exists, we return `true`, else `false`. Forced to be `true` if `--azure-arm` is set, otherwise defaults to `false`.
type: boolean
- key: header-text
description: Text to include as a header comment in generated files (magic strings:MICROSOFT_MIT, MICROSOFT_APACHE, MICROSOFT_MIT_NO_VERSION, MICROSOFT_APACHE_NO_VERSION, MICROSOFT_MIT_NO_CODEGEN)
type: string
- key: openapi-type
description: 'Open API Type: "arm" or "data-plane"'
type: string
- key: max-memory-size
type: string
description: Increases the maximum memory size in MB used by Node.js when running AutoRest (translates to the Node.js parameter --max-old-space-size)
- key: output-converted-oai3
type: string
description: If enabled and the input-files are `swager 2.0` this will output the resulting OpenAPI3.0 converted files to the `output-folder`
- key: title
type: string
description: Override the service client's name listed in the swagger under `title`.
- key: override-client-name
type: string
description: Name to use for the generated client type. By default, uses the value of the 'Title' field from the input files
_autorest-core-1:
categoryFriendlyName: Core Functionality
description: "> While AutoRest can be extended arbitrarily by 3rd parties (say, with a custom generator),\n> we officially support and maintain the following functionality.\n> More specific help is shown when combining the following switches with `--help` ."
settings:
- key: csharp
description: generate C# client code
- key: go
description: generate Go client code
- key: java
description: generate Java client code
- key: python
description: generate Python client code
- key: az
description: generate Azure CLI code
- key: nodejs
description: generate NodeJS client code
- key: typescript
description: generate TypeScript client code
- key: ruby
description: generate Ruby client code
- key: php
description: generate PHP client code
- key: azureresourceschema
description: generate Azure resource schemas
- key: model-validator
description: validates an OpenAPI document against linked examples (see https://github.com/Azure/azure-rest-api-specs/search?q=x-ms-examples )
# - key: semantic-validator
# description: validates an OpenAPI document semantically
- key: azure-validator
description: validates an OpenAPI document against guidelines to improve quality (and optionally Azure guidelines)
```
Note: We don't load anything if `--help` is specified.

Просмотреть файл

@ -2,84 +2,326 @@ import { AutorestNormalizedConfiguration } from "../autorest-normalized-configur
import { ConfigurationSchemaProcessor } from "./processor";
import { RawConfiguration } from "./types";
export const AUTOREST_CONFIGURATION_CATEGORIES = {
logging: {
name: "Logging",
},
installation: {
name: "Manage installation",
},
core: {
name: "Core Settings",
},
feature: {
name: "Feature flags",
},
extensions: {
name: "Generators and extensions",
description:
"> While AutoRest can be extended arbitrarily by 3rd parties (say, with a custom generator),\n> we officially support and maintain the following functionality.\n> More specific help is shown when combining the following switches with `--help` .",
},
};
export const SUPPORTED_EXTENSIONS_SCHEMA = {
csharp: {
type: "boolean",
category: "extensions",
description: "Generate C# client code",
},
go: {
type: "boolean",
category: "extensions",
description: "Generate Go client code",
},
java: {
type: "boolean",
category: "extensions",
description: "Generate Java client code",
},
python: {
type: "boolean",
category: "extensions",
description: "Generate Python client code",
},
az: {
type: "boolean",
category: "extensions",
description: "Generate Az cli code",
},
typescript: {
type: "boolean",
category: "extensions",
description: "Generate TypeScript client code",
},
azureresourceschema: {
type: "boolean",
category: "extensions",
description: "Generate Azurer resource schemas",
},
"model-validator": {
type: "boolean",
category: "extensions",
description:
"Validates an OpenAPI document against linked examples (see https://github.com/Azure/azure-rest-api-specs/search?q=x-ms-examples ",
},
"azure-validator": {
type: "boolean",
category: "extensions",
description:
"Validates an OpenAPI document against guidelines to improve quality (and optionally Azure guidelines)",
},
} as const;
// Switch next 2 lines to have autocomplete when writting configuration. Make sure to revert otherwise it lose the detailed typing for each option.
// export const AUTOREST_CONFIGURATION_SCHEMA : RootConfigurationSchema<keyof typeof AUTOREST_CONFIGURATION_CATEGORIES> = {
export const AUTOREST_CONFIGURATION_SCHEMA = {
"allow-no-input": { type: "boolean" },
"input-file": { type: "string", array: true },
"exclude-file": { type: "string", array: true },
"base-folder": { type: "string" },
directive: {
array: true,
type: {
from: { type: "string", array: true },
where: { type: "string", array: true },
reason: { type: "string" },
suppress: { type: "string", array: true, deprecated: true },
set: { type: "string", array: true },
transform: { type: "string", array: true },
"text-transform": { type: "string", array: true },
test: { type: "string", array: true },
debug: {
type: "boolean",
description:
"Debug this directive. When set to true autorest will log additional information regarding that directive.",
},
},
},
"declare-directive": {
dictionary: true,
/**
* Verbosity category
*/
verbose: { type: "boolean", category: "logging", description: "Display verbose logging information" },
debug: { type: "boolean", category: "logging", description: "Display debug logging information" },
level: {
type: "string",
category: "logging",
enum: ["debug", "verbose", "information", "warning", "error", "fatal"],
description: "Set logging level",
},
"message-format": {
type: "string",
category: "logging",
description: "Format of logging messages",
enum: ["json", "regular"],
},
/**
* Manage installation category
*/
info: {
type: "boolean",
category: "installation",
description: "Display information about the installed version of autorest and its extensions",
},
"list-available": {
type: "boolean",
category: "installation",
description: "Display available AutoRest versions",
},
reset: {
type: "boolean",
category: "installation",
description: "Removes all autorest extensions and downloads the latest version of the autorest-core extension",
},
preview: {
type: "boolean",
category: "installation",
description: "Enables using autorest extensions that are not yet released",
},
latest: {
type: "boolean",
category: "installation",
description: "Install the latest autorest-core extension",
},
force: {
type: "boolean",
category: "installation",
description: "Force the re-installation of the autorest-core extension and frameworks",
},
version: {
type: "string",
category: "installation",
description: "Use the specified version of the autorest-core extension",
},
use: {
type: "array",
category: "installation",
items: { type: "string" },
description:
"Specify an extension to load and use. Format: --use=<packagename>[@<version>] (e.g. --use=@autorest/modelerfour@~4.19.0)",
},
"use-extension": {
type: "dictionary",
category: "installation",
description: `Specify extension to load and use. Format: {"<packageName>": "<version>"}`,
items: { type: "string" },
},
/**
* Core settings
*/
help: {
type: "boolean",
category: "core",
description: "Display help (combine with flags like --csharp to get further details about specific functionality)",
},
memory: { type: "string", category: "core", description: "Configure max memory allowed for autorest process(s)" },
"input-file": {
type: "array",
category: "core",
description: "OpenAPI file to use as input (use this setting repeatedly to pass multiple files at once)",
items: { type: "string" },
},
"output-folder": {
type: "string",
category: "core",
description: `Target folder for generated artifacts; default: "<base folder>/generated"`,
},
"github-auth-token": {
type: "string",
category: "core",
description: "OAuth token to use when pointing AutoRest at files living in a private GitHub repository",
},
"azure-arm": {
type: "boolean",
category: "core",
description: "Generate code in Azure flavor",
},
"header-text": {
type: "string",
category: "core",
description:
"Text to include as a header comment in generated files (magic strings:MICROSOFTMIT, MICROSOFT_APACHE, MICROSOFT_MIT_NO_VERSION, MICROSOFT_APACHE_NO_VERSION, MICROSOFT_MIT_NOCODEGEN)",
},
"openapi-type": {
type: "string",
category: "core",
description: `Open API Type: "arm" or "data-plane"`,
},
"output-converted-oai3": {
type: "boolean",
category: "core",
description: `If enabled and the input-files are swager 2.0 this will output the resulting OpenAPI3.0 converted files to the output-folder`,
},
"output-artifact": { type: "string", array: true },
require: { type: "string", array: true, description: "Additional configuration files to include." },
"try-require": { type: "string", array: true, description: "Additional configuration files to include." },
stats: { type: "boolean", description: "Output some statistics about current autorest run." },
use: { type: "string", array: true },
"use-extension": { type: "string", dictionary: true },
profile: { type: "string", array: true },
"pass-thru": { type: "string", array: true },
eol: {
type: "string",
category: "core",
enum: ["default", "lf", "crlf"],
description: "Change the end of line character for generated output.",
},
title: {
type: "string",
category: "core",
description: "Override the service client's name listed in the swagger under title.",
},
"override-client-name": {
type: "string",
category: "core",
description:
"Name to use for the generated client type. By default, uses the value of the 'Title' field from the input files",
},
directive: {
type: "array",
category: "core",
items: {
type: "object",
properties: {
from: { type: "array", items: { type: "string" } },
where: { type: "array", items: { type: "string" } },
reason: { type: "string" },
suppress: { type: "array", deprecated: true, items: { type: "string" } },
set: { type: "array", items: { type: "string" } },
transform: { type: "array", items: { type: "string" } },
"text-transform": { type: "array", items: { type: "string" } },
test: { type: "array", items: { type: "string" } },
debug: {
type: "boolean",
description:
"Debug this directive. When set to true autorest will log additional information regarding that directive.",
},
},
},
},
require: {
type: "array",
category: "core",
description: "Additional configuration file(s) to include.",
items: { type: "string" },
},
"try-require": {
type: "array",
category: "core",
description:
"Additional configuration file(s) to try to include. Will not fail if the configuration file doesn't exist.",
items: { type: "string", description: "Additional configuration files to include." },
},
"declare-directive": {
type: "dictionary",
category: "core",
description:
"Declare some reusable directives (https://github.com/Azure/autorest/blob/main/packages/libs/configuration/resources/directives.md#how-it-works)",
items: { type: "string" },
},
"output-artifact": {
type: "array",
category: "core",
description: "Additional artifact type to emit to the output-folder",
items: { type: "string" },
},
"allow-no-input": { type: "boolean" },
"exclude-file": { type: "array", items: { type: "string" } },
"base-folder": { type: "string" },
stats: { type: "boolean", category: "core", description: "Output some statistics about current autorest run." },
profile: {
type: "array",
category: "core",
description: "Reservered for future use.",
items: { type: "string" },
},
"message-format": { type: "string", enum: ["json", "regular"] },
verbose: { type: "boolean" },
debug: { type: "boolean" },
level: { type: "string", enum: ["debug", "verbose", "information", "warning", "error", "fatal"] },
suppressions: {
array: true,
type: {
code: { type: "string" },
from: { type: "string", array: true },
where: { type: "string", array: true },
reason: { type: "string" },
type: "array",
category: "core",
description: "List of warning/error code to ignore.",
items: {
type: "object",
properties: {
code: { type: "string" },
from: { type: "array", items: { type: "string" } },
where: { type: "array", items: { type: "string" } },
reason: { type: "string" },
},
},
},
title: { type: "string" },
"github-auth-token": { type: "string" },
"output-file": { type: "string" },
"output-folder": { type: "string" },
force: {
type: "boolean",
description: "Force updating the version of core even if there is a local version satisfying the requirement.",
},
memory: { type: "string", description: "Configure max memory allowed for autorest process(s)" },
/**
* Feature flags
*/
"deduplicate-inline-models": { type: "boolean", category: "feature", description: "Deduplicate inline models" },
"include-x-ms-examples-original-file": {
type: "boolean",
category: "feature",
description: "Include x-ms-original-file property in x-ms-examples",
},
// Feature flags
"deduplicate-inline-models": { type: "boolean" },
/**
* Ignore.
*/
"pass-thru": {
type: "array",
items: { type: "string" },
},
} as const;
export type AutorestRawConfiguration = RawConfiguration<typeof AUTOREST_CONFIGURATION_SCHEMA> & {
[key: string]: any;
};
export const autorestConfigurationProcessor = new ConfigurationSchemaProcessor(AUTOREST_CONFIGURATION_SCHEMA);
export const AUTOREST_CONFIGURATION_DEFINITION = {
categories: AUTOREST_CONFIGURATION_CATEGORIES,
schema: AUTOREST_CONFIGURATION_SCHEMA,
};
export const AUTOREST_CONFIGURATION_DEFINITION_FOR_HELP = {
categories: AUTOREST_CONFIGURATION_CATEGORIES,
// SUPPORTED_EXTENSIONS_SCHEMA can either be a flag to enable or a scope which cause issue with the validation.
schema: { ...AUTOREST_CONFIGURATION_SCHEMA, ...SUPPORTED_EXTENSIONS_SCHEMA },
};
export const autorestConfigurationProcessor = new ConfigurationSchemaProcessor(AUTOREST_CONFIGURATION_DEFINITION);
export const AUTOREST_INITIAL_CONFIG: AutorestNormalizedConfiguration =
autorestConfigurationProcessor.getInitialConfig();

Просмотреть файл

@ -17,19 +17,25 @@ const TestSchema = {
enum: ["one", "two", "three"],
},
numberArray: {
type: "number",
array: true,
type: "array",
items: { type: "number" },
},
numberDict: {
type: "number",
dictionary: true,
type: "dictionary",
items: { type: "number" },
},
nested: {
nestedNumber: { type: "number" },
type: "object",
properties: {
nestedNumber: { type: "number" },
},
},
} as const;
const processor = new ConfigurationSchemaProcessor(TestSchema);
const processor = new ConfigurationSchemaProcessor({
schema: TestSchema,
categories: { default: { name: "default" } },
});
const logger = createMockLogger();

Просмотреть файл

@ -4,13 +4,16 @@ import { flatMap } from "lodash";
import {
ConfigurationProperty,
ConfigurationSchema,
InferredPrimitiveType,
InferredProcessedPrimitiveType,
InferredProcessedType,
InferredRawPrimitiveType,
InferredRawType,
ProcessedConfiguration,
RawConfiguration,
ConfigurationSchemaDefinition,
PrimitiveConfigurationProperty,
} from "./types";
import { RootConfigurationSchema } from ".";
export enum ProcessingErrorCode {
UnknownProperty = "unknownProperty",
@ -37,21 +40,21 @@ export interface ProcessConfigurationOptions {
logger: AutorestLogger;
}
export class ConfigurationSchemaProcessor<S extends ConfigurationSchema> {
public constructor(private schema: S) {}
export class ConfigurationSchemaProcessor<C extends string, S extends RootConfigurationSchema<C>> {
public constructor(private def: ConfigurationSchemaDefinition<C, S>) {}
public processConfiguration(
configuration: RawConfiguration<S>,
options: ProcessConfigurationOptions,
): Result<ProcessedConfiguration<S>> {
return processConfiguration(this.schema, [], configuration, options);
return processConfiguration(this.def.schema, [], configuration, options);
}
/**
* Returns an empty config with all array property to be able to work with @see mergeConfigurations.
*/
public getInitialConfig(): Readonly<ProcessedConfiguration<S>> {
return getInitialConfig(this.schema);
return getInitialConfig(this.def.schema);
}
}
@ -59,10 +62,10 @@ function getInitialConfig<S extends ConfigurationSchema>(schema: S): Readonly<Pr
const config: any = {};
for (const [key, value] of Object.entries(schema)) {
if (value.array) {
if (value.type === "array") {
config[key] = [];
} else if (!("type" in value)) {
const nested = getInitialConfig(value);
} else if (value.type === "object") {
const nested = getInitialConfig(value.properties);
if (Object.keys(nested).length > 0) {
config[key] = nested;
}
@ -93,7 +96,7 @@ function processConfiguration<S extends ConfigurationSchema>(
continue;
}
const propertyResult = processProperty(propertySchema as any, propertyPath, value, options);
const propertyResult = processProperty(propertySchema, propertyPath, value, options);
if (isErrorResult(propertyResult)) {
errors.push(...propertyResult.errors);
} else {
@ -116,13 +119,13 @@ function processProperty<T extends ConfigurationProperty>(
value: InferredRawType<T>,
options: ProcessConfigurationOptions,
): Result<InferredProcessedType<T>> {
if (schema.array) {
if (schema.type === "array") {
if (value === undefined) {
return { value: [] as any };
}
if (Array.isArray(value)) {
const result = value.map((x, i) => processPrimitiveProperty(schema, [...path, i.toString()], x, options));
const result = value.map((x, i) => processPrimitiveProperty(schema.items, [...path, i.toString()], x, options));
const values = result.filter(isNotErrorResult).map((x) => x.value);
const errors = flatMap(result.filter(isErrorResult).map((x) => x.errors));
if (errors.length > 0) {
@ -130,14 +133,14 @@ function processProperty<T extends ConfigurationProperty>(
}
return { value: values as any };
} else {
const result = processPrimitiveProperty(schema, path, value as InferredRawPrimitiveType<T>, options);
const result = processPrimitiveProperty(schema.items, path, value, options);
if (isErrorResult(result)) {
return result;
} else {
return { value: [result.value] as any };
}
}
} else if (schema.dictionary) {
} else if (schema.type === "dictionary") {
if (value === undefined) {
return { value: {} as any };
}
@ -148,7 +151,7 @@ function processProperty<T extends ConfigurationProperty>(
const result: any = {};
for (const [key, dictValue] of Object.entries<any>(value ?? {})) {
const prop = processPrimitiveProperty(schema, [...path, key], dictValue, options);
const prop = processProperty(schema.items, [...path, key], dictValue, options);
if ("errors" in prop) {
return { errors: prop.errors };
}
@ -157,62 +160,60 @@ function processProperty<T extends ConfigurationProperty>(
return { value: result };
} else {
return processPrimitiveProperty(schema, path, value as InferredRawPrimitiveType<T>, options) as any;
return processPrimitiveProperty(schema, path, value as any, options) as any;
}
}
function processPrimitiveProperty<T extends ConfigurationProperty>(
function processPrimitiveProperty<T extends PrimitiveConfigurationProperty>(
schema: T,
path: string[],
value: InferredRawPrimitiveType<T>,
options: ProcessConfigurationOptions,
): Result<InferredPrimitiveType<T>> {
if (schema.type === "number") {
if (typeof value !== "number") {
return {
errors: [createInvalidTypeError(value, "number", path)],
};
}
return { value } as any;
}
if (schema.type === "boolean") {
if (typeof value !== "boolean") {
return {
errors: [createInvalidTypeError(value, "boolean", path)],
};
}
return { value } as any;
}
if (schema.type === "string") {
if (typeof value !== "string") {
return {
errors: [createInvalidTypeError(value, "string", path)],
};
}
if (schema.enum) {
if (!schema.enum.includes(value)) {
const serializedValue = inspect(value);
): Result<InferredProcessedPrimitiveType<T>> {
switch (schema.type) {
case "object":
return processConfiguration<any>(schema.properties, path, value as any, options) as any;
case "number": {
if (typeof value !== "number") {
return {
errors: [
{
code: ProcessingErrorCode.InvalidType,
message: `Expected a value to be in [${schema.enum
.map((x) => `'${x}'`)
.join(",")}] but got ${serializedValue}`,
path,
},
],
errors: [createInvalidTypeError(value, "number", path)],
};
}
return { value } as any;
}
return { value } as any;
}
case "boolean":
if (typeof value !== "boolean") {
return {
errors: [createInvalidTypeError(value, "boolean", path)],
};
}
return { value } as any;
case "string": {
if (typeof value !== "string") {
return {
errors: [createInvalidTypeError(value, "string", path)],
};
}
// Means this is a nested configuration schema
return processConfiguration<any>(schema, path, value as any, options) as any;
if (schema.enum) {
if (!schema.enum.includes(value)) {
const serializedValue = inspect(value);
return {
errors: [
{
code: ProcessingErrorCode.InvalidType,
message: `Expected a value to be in [${schema.enum
.map((x) => `'${x}'`)
.join(",")}] but got ${serializedValue}`,
path,
},
],
};
}
}
return { value } as any;
}
}
}
function createInvalidTypeError(value: unknown, expectedType: string, path: string[]) {

Просмотреть файл

@ -1,85 +1,137 @@
export type ConfigurationSchema = {
readonly [key: string]: ConfigurationSchema | ConfigurationProperty;
export type ConfigurationSchemaDefinition<C extends string, S extends RootConfigurationSchema<C>> = {
readonly categories: {
[key in C]: CategoryDefinition;
};
readonly schema: S;
};
export type ConfigurationPropertyType = "string" | "number" | "boolean" | ConfigurationSchema;
export type RootConfigurationSchema<C extends string> = {
readonly [key: string]: RootConfigurationProperty<C>;
};
export type ConfigurationProperty = {
export type ConfigurationSchema = {
readonly [key: string]: ConfigurationProperty;
};
export type ConfigurationPropertyType = "string" | "number" | "boolean" | "array" | "dictionary" | "object";
export type CategoryDefinition = {
name: string;
description?: string;
};
export type RootConfigurationProperty<C extends string> = ConfigurationProperty & {
/**
* Type.
* Category for the configuration. For documentation purposes.
*/
readonly category?: C;
};
export type ConfigurationPropertyBase = {
readonly type: ConfigurationPropertyType;
/**
* If this is an array of the type.
*/
readonly array?: boolean;
/**
* If this is a map/dictionary of the type.
*/
readonly dictionary?: boolean;
/**
* List of supported values. Only supported for type: string.
*/
readonly enum?: readonly string[];
/**
* Description for the flag.
* Description for the configuration.
*/
readonly description?: string;
/**
* Mark this config flag as deprecated.It will log a warning when used.
* Mark this config as deprecated.It will log a warning when used.
*/
readonly deprecated?: boolean;
};
export type StringConfigurationProperty = ConfigurationPropertyBase & {
type: "string";
/**
* List of supported values. Only supported for type: string.
*/
readonly enum?: readonly string[];
};
export type NumberConfigurationProperty = ConfigurationPropertyBase & {
type: "number";
};
export type BooleanConfigurationProperty = ConfigurationPropertyBase & {
type: "boolean";
};
export type ArrayConfigurationProperty = ConfigurationPropertyBase & {
type: "array";
items: PrimitiveConfigurationProperty;
};
export type DictionaryConfigurationProperty = ConfigurationPropertyBase & {
type: "dictionary";
items: PrimitiveConfigurationProperty;
};
export type ObjectConfigurationProperty = ConfigurationPropertyBase & {
type: "object";
properties: ConfigurationSchema;
};
export type ConfigurationProperty =
| PrimitiveConfigurationProperty
| ArrayConfigurationProperty
| DictionaryConfigurationProperty;
export type PrimitiveConfigurationProperty =
| ObjectConfigurationProperty
| StringConfigurationProperty
| NumberConfigurationProperty
| BooleanConfigurationProperty;
export type EnumType<T extends ReadonlyArray<string>> = T[number];
// prettier-ignore
export type InferredProcessedType<T> =
T extends { array: true } ? InferredPrimitiveType<T>[]
: T extends { dictionary: true } ? Record<string, InferredPrimitiveType<T>>
: InferredPrimitiveType<T>;
export type ProcessedConfiguration<S extends ConfigurationSchema> = {
[K in keyof S]: InferredProcessedType<S[K]>;
};
// prettier-ignore
export type InferredPrimitiveType<T> =
export type InferredProcessedType<T extends ConfigurationProperty> =
T extends ArrayConfigurationProperty ? InferredProcessedArrayType<T["items"]>
: T extends DictionaryConfigurationProperty ? InferredProcessedDictionaryType<T["items"]>
: T extends PrimitiveConfigurationProperty ? InferredProcessedPrimitiveType<T>
: never;
export type InferredProcessedArrayType<T extends PrimitiveConfigurationProperty> = InferredProcessedPrimitiveType<T>[];
export type InferredProcessedDictionaryType<T extends PrimitiveConfigurationProperty> = Record<
string,
InferredProcessedPrimitiveType<T>
>;
// prettier-ignore
export type InferredProcessedPrimitiveType<T extends PrimitiveConfigurationProperty> =
T extends { type: "string", enum: ReadonlyArray<string> } ? EnumType<T["enum"]>
: T extends { type: "string" } ? string
: T extends { type: "number" } ? number
: T extends { type: "boolean" } ? boolean
: T extends { type: ConfigurationSchema } ? ProcessedConfiguration<T["type"]>
: T extends ObjectConfigurationProperty ? ProcessedConfiguration<T["properties"]>
: never;
// prettier-ignore
export type InferredRawType<T> =
T extends { array: true } ? NonNullable<InferredRawPrimitiveType<T>>[] | InferredRawPrimitiveType<T>
: T extends { dictionary: true } ? Record<string, InferredRawPrimitiveType<T>>
: InferredRawPrimitiveType<T>;
export type InferredRawType<T extends ConfigurationProperty> =
T extends ArrayConfigurationProperty ? InferredRawArrayType<T["items"]>
: T extends DictionaryConfigurationProperty ? InferredRawDictionaryType<T["items"]>
: T extends PrimitiveConfigurationProperty ? InferredRawPrimitiveType<T>
: never;
export type InferredRawArrayType<T extends PrimitiveConfigurationProperty> =
| NonNullable<InferredRawPrimitiveType<T>>[]
| InferredRawPrimitiveType<T>;
export type InferredRawDictionaryType<T extends ConfigurationProperty> = Record<string, InferredRawType<T>>;
// prettier-ignore
export type InferredRawPrimitiveType<T> =
export type InferredRawPrimitiveType<T extends PrimitiveConfigurationProperty> =
T extends { type: "string", enum: ReadonlyArray<string> } ? EnumType<T["enum"]>
: T extends { type: "string" } ? string | undefined
: T extends { type: "number" } ? number | undefined
: T extends { type: "boolean" } ? boolean | undefined
: T extends { type: ConfigurationSchema } ? RawConfiguration<T["type"]> | undefined
: T extends { type: "string" } ? string | undefined
: T extends { type: "number" } ? number | undefined
: T extends { type: "boolean" } ? boolean | undefined
: T extends ObjectConfigurationProperty ? RawConfiguration<T["properties"]> | undefined
: never;
export type ProcessedConfiguration<S extends ConfigurationSchema> = {
[K in keyof S]: S[K] extends ConfigurationProperty
? InferredProcessedType<S[K]>
: S[K] extends ConfigurationSchema
? ProcessedConfiguration<S[K]>
: never;
};
export type RawConfiguration<S extends ConfigurationSchema> = {
[K in keyof S]?: S[K] extends ConfigurationProperty
? InferredRawType<S[K]>
: S[K] extends ConfigurationSchema
? RawConfiguration<S[K]>
: never;
[K in keyof S]?: InferredRawType<S[K]>;
};