Move @autorest/modelerfour package (#3806)

This commit is contained in:
Timothee Guerin 2021-01-25 15:55:42 -08:00 коммит произвёл GitHub
Родитель 1da414a793
Коммит 3b80bf9506
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
48 изменённых файлов: 8361 добавлений и 420 удалений

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -65,7 +65,7 @@
"source-map-support": "^0.5.19",
"safe-buffer": "5.2.0",
"prettier": "~2.2.1",
"eslint-plugin-prettier": "~3.1.4"
"eslint-plugin-prettier": "~3.2.0"
},
"static-link": {
"entrypoints": [],

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

@ -61,7 +61,7 @@
"@azure-tools/uri": "~3.0.0",
"@azure-tools/datastore": "~4.1.0",
"@azure-tools/oai2-to-oai3": "~4.2.0",
"@azure-tools/codegen": "~2.5.0",
"@azure-tools/codegen": "2.5.290",
"@azure-tools/openapi": "~3.0.0",
"@azure-tools/linq": "~3.1.0",
"@azure-tools/tasks": "~3.0.0",
@ -82,7 +82,7 @@
"z-schema": "^5.0.0",
"safe-buffer": "5.2.0",
"prettier": "~2.2.1",
"eslint-plugin-prettier": "~3.1.4"
"eslint-plugin-prettier": "~3.2.0"
},
"static-link": {
"entrypoints": [],

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

@ -0,0 +1,9 @@
---
# configure plugins first
parser: "@typescript-eslint/parser"
plugins:
- "@typescript-eslint"
# then inherit the common settings
extends:
- "../../../.default-eslintrc.yaml"

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

@ -0,0 +1,20 @@
!dist/**/*
src/
dist/test/
test/
package/
.npmignore
tsconfig.json
*.ts
.eslint*
!*.d.ts
*.tgz
.vscode
.scripts
attic/
generated/
notes.md
Examples/
samples/
*.log
package-deps.json

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

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE

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

@ -0,0 +1,103 @@
# Changelog for ModelerFour
#### 4.16.0 _(unreleased)_
- **Fix** Missing description in responses. ([PR 370](https://github.com/Azure/autorest.modelerfour/pull/370))
- **Feature** Added new flag `always-create-accept-parameter` to enable/disable accept param auto generation. ([PR 366](https://github.com/Azure/autorest.modelerfour/pull/366))
- **Fix** Allow request with body being a file and `application/json` content-type. ([PR 363](https://github.com/Azure/autorest.modelerfour/pull/363))
- **Fix** Dictionaries of dictionaries not being modeled as such(`dict[str, object]` instead of `dict[str, dict[str, str]]`). ([PR 372](https://github.com/Azure/autorest.modelerfour/pull/372))
- **Fix** Issue with sibling models(Model just being a ref of another) causing circular dependency exception. ([PR 375](https://github.com/Azure/autorest.modelerfour/pull/375))
- **Fix** Issue with duplicates schemas names due to consequtive name duplicate removal. ([PR 374](https://github.com/Azure/autorest.modelerfour/pull/374))
#### 4.15.x
- Schemas with `x-ms-enum`'s `modelAsString` set to `true` will now be represented as `ChoiceSchema` even with a single value.
- `Accept` headers are now automatically added to operations having responses with content types
- Added `always-seal-x-ms-enum` settings to always create `SealedChoiceSchema` when an `x-ms-enum` is encountered
#### 4.14.x
- added `exception` SchemaContext for `usage` when used as an exception response
- changed `output` SchemaContext for `usage` to no longer include exception response uses
#### 4.13.x
- add security info (checks to see if `input.components?.securitySchemes` has any content)
- sync version of m4 and perks/codemodel == 4.13.x
- adding quality prechecker step as a way to test the OAI document for quality before modelerfour runs.
- report duplicate parents via allOf as an error.
- added `modelerfour.lenient-model-deduplication` to cause schemas with duplicated names to be renamed with an "AutoGenerated" suffix. Note that this is a *temporary* measuer that should only be used when Swaggers cannot be updated easily. This option will be removed in a future version of Modeler Four.
#### 4.12.x
- updated CI to build packages
- any is in a category in schemas
- times is a new category in schemas (not populated yet, next build)
- polymorphic payloads are not flattened (when it's the class that declares the discriminator)
- readonly is pulled from the schema if it's there
- body parameters should have the required flag set correctly
- content-type is now a header parameter (wasn't set before)
- added `modelerfour.always-create-content-type-parameter` to always get the content type parameter even when there are only one option.
- add support for x-ms-api-version extension to force enabling/disabling parameter to be treated as an api-version parameter
- the checker plugin will now halt on errors (can be disabled by `modelerfour.additional-checks: false`)
- when an enum without type is presented, if the values are all strings, assume 'string'
- flatten parents first for consistency
- added choiceType for content-type schema
#### 4.6.x
- add additional checks for empty names, collisions
- fix errant processing on APString => Apstring
- x-ms-client-name fixes on parameters
- added setting for `preserve-uppercase-max-length` to preserve uppercase words up to a certain length.
#### 4.5.x
- static linking libraries for stability
- processed all names in namer, styles can be set in config (see below):
- support overrides in namer
- static linked dependency
#### 4.4.x
- parameter grouping
- some namer changes
#### 4.3.x
- flattening (model and payload) enabled.
- properties should respect x-ms-client-name (many fixes)
- global parameters should try to be in order of original spec
- filter out 'x-ms-original' from extensions
- add serializedName for host parameters
- make sure reused global parameter is added to method too
- processed values in constants/enums a bit better, support AnySchema for no type/format
- support server variable parameters as method unless they have x-ms-parameter-location
#### 4.2.75 - bug fixes:
- add `style` to parameters to support collection format
- `potential-breaking-change` Include common paramters from oai/path #68 (requires fix from autorest-core 3.0.6160+ )
- propogate extensions from server parameters (ie, x-ms-skip-url-encoding) #61
- `potential-breaking-change` make operation groups case insensitive. #59
- `potential-breaking-change` sealedChoice/Choice selection was backwards ( was creating a sealedchoice schema for modelAsString:true and vice versa) #62
- `potential-breaking-change` drop constant schema from response, use constantschema's valueType instead. #63
- `potential-breaking-change` fix body parameter marked as required when not marked so in spec. #64
#### 4.1.60 - add missing serializedName on parameters
- query parameters should have a serializedName so that they don't rely on the cosmetic name property.
#### 4.1.58 - Breaking change:
- version bump, change your configuration to specify version `~4.1.0` or greater
```
use-extension:
"@autorest/modelerfour" : "~4.1.0"
```
- each Http operation (via `.protocol.http`) will now have a separate `path` and `uri` properties.
<br>Both are still templates, and will have parameters.
<br>The parameters for the `uri` property will have `in` set to `ParameterLocation.Uri`
<br>The parameters for the `path` property will continue to have `in` set to `ParameterLocation.Path`
- autorest-core recently added an option to aggressively deduplicate inline models (ie, ones without a name)
and modeler-four based generator will have that enabled by default. (ie `deduplicate-inline-models: true`)
<br>This may increase deduplication time on extremely large openapi models.
- this package contains the initial code for the flattener plugin, however it is not yet enabled.
- updated `@azure-tools/codemodel` package to `3.0.241`:
<br>`uri` (required) was added to `HttpRequest`
<br>`flattenedNames` (optional) was added to `Property` (in anticipation of supporting flattening)

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

@ -0,0 +1,14 @@
#!/usr/bin/env node
// load modules from static linker filesystem.
try {
if (
process.argv.indexOf("--no-static-loader") === -1 &&
process.env["no-static-loader"] === undefined &&
require("fs").existsSync(`${__dirname}/../dist/static-loader.js`)
) {
require(`${__dirname}/../dist/static-loader.js`).load(`${__dirname}/../dist/static_modules.fs`);
}
require(`${__dirname}/../dist/src/main.js`);
} catch (e) {
console.error(e);
}

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

@ -0,0 +1,26 @@
// @ts-check
/** @type {jest.InitialOptions} */
const config = {
transform: {
"^.+\\.ts$": "ts-jest",
},
moduleFileExtensions: ["ts", "js", "json", "node"],
moduleNameMapper: {},
collectCoverage: true,
collectCoverageFrom: ["src/**/*.ts", "!**/node_modules/**"],
coverageReporters: ["json", "lcov", "cobertura", "text", "html", "clover"],
coveragePathIgnorePatterns: ["/node_modules/", ".*/test/.*"],
modulePathIgnorePatterns: ["<rootDir>/sdk"],
globals: {
"ts-jest": {
tsconfig: "tsconfig.json",
},
},
setupFilesAfterEnv: ["<rootDir>/test/setupJest.ts"],
testMatch: ["<rootDir>/test/**/*.test.ts"],
verbose: true,
testEnvironment: "node",
};
module.exports = config;

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

@ -0,0 +1,89 @@
{
"name": "@autorest/modelerfour",
"version": "4.15.0",
"patchOffset": -765,
"description": "AutoRest Modeler Version Four (component)",
"directories": {
"doc": "docs"
},
"engines": {
"node": ">=10.12.0"
},
"main": "dist/exports.js",
"typings": "dist/exports.d.ts",
"scripts": {
"start": "node --max_old_space_size=4096 ./entrypoints/main.js",
"debug": "node --max_old_space_size=4096 --inspect-brk=localhost:9229 ./entrypoints/main.js",
"fix": "eslint ./src --fix --ext .ts",
"lint": "eslint ./src --ext .ts --max-warnings=0",
"static-link": "static-link --no-node-modules --debug",
"watch": "tsc -p . --watch",
"build": "tsc -p .",
"prepack": "npm run static-link && npm run build",
"test": "jest --watch --coverage=false",
"test:ci": "jest --ci"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Azure/autorest.modelerfour.git"
},
"keywords": [
"AutoRest",
"ModelerFour"
],
"author": "",
"license": "MIT",
"bugs": {
"url": "https://github.com/Azure/autorest.modelerfour/issues"
},
"homepage": "https://github.com/Azure/autorest.modelerfour/tree/master/modelerfour#readme",
"readme": "https://github.com/Azure/autorest.modelerfour/tree/master/modelerfour/readme.md",
"devDependencies": {
"@types/js-yaml": "~4.0.0",
"@types/node": "~14.14.20",
"@types/jest": "^26.0.20",
"typescript": "~3.9.7",
"@typescript-eslint/eslint-plugin": "^4.12.0",
"@typescript-eslint/parser": "^4.12.0",
"eslint": "^7.17.0",
"@azure-tools/async-io": "~3.0.0",
"source-map-support": "^0.5.19",
"@microsoft.azure/autorest.testserver": "~2.10.46",
"@azure-tools/uri": "~3.0.0",
"js-yaml": "3.13.1",
"jest": "^26.6.3",
"jest-snapshot": "~26.6.2",
"expect": "~26.6.2",
"ts-jest": "^26.4.4",
"@azure-tools/codegen": "2.5.290",
"@azure-tools/codegen-csharp": "~3.0.0",
"@azure-tools/autorest-extension-base": "~3.1.0",
"@azure-tools/codemodel": "~4.13.0",
"@azure-tools/tasks": "~3.0.0",
"@azure-tools/openapi": "~3.0.0",
"@azure-tools/datastore": "~4.1.0",
"@azure-tools/linq": "~3.1.0",
"static-link": "^0.3.0",
"chalk": "2.3.0",
"recursive-diff": "~1.0.6",
"prettier": "~2.2.1",
"eslint-plugin-prettier": "~3.2.0"
},
"static-link": {
"entrypoints": [],
"dependencies": {
"js-yaml": "3.13.1",
"@azure-tools/codegen": "~2.4.0",
"@azure-tools/codegen-csharp": "~3.0.0",
"@azure-tools/autorest-extension-base": "~3.1.0",
"@azure-tools/codemodel": "~4.13.0",
"@azure-tools/tasks": "~3.0.0",
"@azure-tools/openapi": "~3.0.0",
"@azure-tools/datastore": "~4.1.0",
"@azure-tools/linq": "~3.1.0",
"source-map-support": "^0.5.19",
"recursive-diff": "~1.0.6"
}
},
"dependencies": {}
}

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

@ -0,0 +1,234 @@
# AutoRest Modeler Four
## Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
### Autorest plugin configuration
- Please don't edit this section unless you're re-configuring how the powershell extension plugs in to AutoRest
AutoRest needs the below config to pick this up as a plug-in - see https://github.com/Azure/autorest/blob/master/docs/developer/architecture/AutoRest-extension.md
### ModelFour Options
You can specify the following options in your configuration for modelerfour:
~~~ markdown
``` yaml
modelerfour:
# this will speed up the serialization if you explicitly say you do or do not want yaml tags in the model
# default - both
emit-yaml-tags: undefined|true|false
# this will flatten modelers marked with 'x-ms-client-flatten'
# defaults to false if not specified
flatten-models: false|true
# this will flatten parameters when payload-flattening-threshold is specified (or marked in the input spec)
# defaults to false if not specified
flatten-payloads: false|true
# setting this to false will skip parameter flattening
# for operations that have multiple requests (ie, JSON and BINARY)
multiple-request-parameter-flattening: true|false
# this runs a pre-namer step to clean up names
# defaults to true if not specified
prenamer: true|false
# relaxes schema duplication checks to allow schemas with the
# same name and renames duplicate schema names with a suffix
# of "AutoGenerated" with an additional numeric suffix if more
# than 2 duplicates of the same name are detected.
#
# defaults to false if not specified.
#
# NOTE: This parameter is a temporary workaround and will be
# removed in a future release!
lenient-model-deduplication: false|true
# does a check to see if names in schemas/enums/etc will collide
# off by default
resolve-schema-name-collisons: false|true
# if you want to keep the flattened models even if they are not used
# off by default
keep-unused-flattened-models: false|true
# merges response headers into response objects
# defaults to false if not specified
# not implemented
merge-response-headers: false|true
# enables parameter grouping via x-ms-parameter-grouping
# defaults to false if not specified
group-parameters: false|true
# some additional sanity checks to help debugging
# defaults to false
additional-checks: true|false
# always create the content-type parameter for binary requests
# when it's only one possible value, make it a constant.
always-create-content-type-parameter: false|true
# always create the Accept parameter
always-create-accept-parameter: true|false
# always create SealedChoiceSchema for x-ms-enum schemas no matter
# what the settings are. This can be used to smooth migration from
# remodeler to modelerfour.
always-seal-x-ms-enum: false|true
# In the case where a type only definition is to inherit another type remove it.
# e.g. ChildSchema: {allOf: [ParentSchema]}.
# In this case ChildSchema will be removed and all reference to it will be updated to point to ParentSchema
remove-empty-child-schemas: false|true
# customization of the identifier normalization and naming provided by the prenamer.
# pascal|pascalcase - MultiWordIdentifier
# camel|camelcase - multiWordIdentifier
# snake|snakecase - multi_word_identifier
# upper|uppercase - MULTI_WORD_IDENTIFIER
# kebab|kebabcase - multi-word-identifier
# space|spacecase - spaces between recognized words
# default is the first one in the list below:
# you can prefix or postfix a formatted name with + (ie, '_ + camel' or 'pascal + _' )
naming:
preserve-uppercase-max-length: <number> #defaults to 3
parameter: camel|pascal|snake|upper|kebab|space
property: camel|pascal|snake|upper|kebab|space
operation: pascal|camel|snake|upper|kebab|space
operationGroup: pascal|camel|snake|upper|kebab|space
choice: pascal|camel|snake|upper|kebab|space
choiceValue: pascal|camel|snake|upper|kebab|space
constant: pascal|camel|snake|upper|kebab|space
type: pascal|camel|snake|upper|kebab|space
client: pascal|camel|snake|upper|kebab|space
local: _ + camel
global: camel
override: # a key/value mapping of names to force to a certain value
cmyk : CMYK
$host: $host
LRO: LRO
```
~~~
Default options:
```yaml
modelerfour:
always-create-accept-parameter: true
```
#### ModelerFour
``` yaml
pipeline-model: v3
modelerfour-loaded: true
```
``` yaml !$(enable-deduplication)
# By default, modeler-four based generators will not use the deduplicator or subset reducer
# if we need to easily disable this set the enable-deduplication flag.
pass-thru:
- model-deduplicator
- subset-reducer
```
``` yaml
modelerfour:
naming:
override: # defaults
cmyk : CMYK
$host: $host
pipeline:
prechecker:
input: openapi-document/multi-api/identity
modelerfour:
input:
- prechecker
modelerfour/new-transform:
input: modelerfour
modelerfour/flattener:
input: modelerfour/new-transform
modelerfour/flattener/new-transform:
input: modelerfour/flattener
modelerfour/grouper:
input: modelerfour/flattener/new-transform
modelerfour/grouper/new-transform:
input: modelerfour/grouper
modelerfour/pre-namer:
input: modelerfour/grouper/new-transform
modelerfour/pre-namer/new-transform:
input: modelerfour/pre-namer
modelerfour/checker:
input:
- modelerfour/pre-namer/new-transform
- prechecker
modelerfour/identity:
input: modelerfour/checker
modelerfour/emitter:
input: modelerfour/identity
scope: scope-modelerfour/emitter
modelerfour/notags/emitter:
input: modelerfour/identity
scope: scope-modelerfour/notags/emitter
scope-modelerfour/emitter: # writing to disk settings
input-artifact: code-model-v4
is-object: true # tells autorest that it is an object graph instead of a text document
output-uri-expr: | # forces filename if it gets written to disk.
"code-model-v4.yaml"
scope-modelerfour/notags/emitter: # writing to disk settings
input-artifact: code-model-v4-no-tags
is-object: true # tells autorest that it is an object graph instead of a text document
output-uri-expr: | # forces filename if it gets written to disk.
"code-model-v4-no-tags.yaml"
# the default preference for modeler-four based generators is to deduplicate inline models fully.
# this may impact performance on extremely large models with a lot of inline schemas.
deduplicate-inline-models: true
```
``` yaml $(inspector)
pipeline:
inspector/codemodel/reset-identity:
input:
- prechecker
- modelerfour/identity
- inspector
to: inspect-document
inspector/emitter:
input:
- inspector/codemodel/reset-identity
```

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

@ -0,0 +1,85 @@
import {
CodeModel,
Schema,
ObjectSchema,
isObjectSchema,
SchemaType,
Property,
ParameterLocation,
Operation,
Parameter,
VirtualParameter,
getAllProperties,
ImplementationLocation,
DictionarySchema,
} from "@azure-tools/codemodel";
import { Session } from "@azure-tools/autorest-extension-base";
import { values, items, length, Dictionary, refCount, clone } from "@azure-tools/linq";
import { ModelerFourOptions } from "../modeler/modelerfour-options";
export class Checker {
codeModel: CodeModel;
options: ModelerFourOptions = {};
constructor(protected session: Session<CodeModel>) {
this.codeModel = session.model; // shadow(session.model, filename);
}
async init() {
// get our configuration for this run.
this.options = await this.session.getValue("modelerfour", {});
return this;
}
checkOperationGroups() {
for (const dupe of values(this.codeModel.operationGroups)
.select((each) => each.language.default.name)
.duplicates()) {
this.session.error(`Duplicate Operation group '${dupe}' detected .`, []);
}
}
checkOperations() {
for (const group of this.codeModel.operationGroups) {
for (const dupe of values(group.operations)
.select((each) => each.language.default.name)
.duplicates()) {
this.session.error(`Duplicate Operation '${dupe}' detected.`, []);
}
}
}
checkSchemas() {
const allSchemas = values(<Dictionary<Array<Schema>>>(<any>this.codeModel.schemas))
.selectMany((schemas) => (Array.isArray(schemas) ? values(schemas) : []))
.toArray();
for (const each of values(allSchemas).where((each) => !each.language.default.name)) {
this.session.warning(`Schema Missing Name '${JSON.stringify(each)}'.`, []);
}
const types = values(<Array<Schema>>this.codeModel.schemas.objects)
.concat(values(this.codeModel.schemas.groups))
.concat(values(this.codeModel.schemas.choices))
.concat(values(this.codeModel.schemas.sealedChoices))
.toArray();
for (const dupe of values(types).duplicates((each) => each.language.default.name)) {
this.session.error(`Duplicate object schemas with '${dupe.language.default.name}' name detected.`, []);
}
/* for (const dupe of values(this.codeModel.schemas.numbers).select(each => each.type).duplicates()) {
this.session.error(`Duplicate '${dupe}' detected.`, []);
}; */
}
process() {
if (this.options["additional-checks"] !== false) {
this.checkOperationGroups();
this.checkOperations();
this.checkSchemas();
}
return this.codeModel;
}
}

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

@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { serialize } from "@azure-tools/codegen";
import { Host, startSession } from "@azure-tools/autorest-extension-base";
import { codeModelSchema, CodeModel } from "@azure-tools/codemodel";
import { Checker } from "./checker";
export async function processRequest(host: Host) {
const debug = (await host.GetValue("debug")) || false;
try {
const session = await startSession<CodeModel>(host, {}, codeModelSchema);
const options = <any>await session.getValue("modelerfour", {});
// process
const plugin = await new Checker(session).init();
// go!
const result = plugin.process();
// throw on errors.
if (!(await session.getValue("no-errors", false))) {
session.checkpoint();
}
// output the model to the pipeline
if (options["emit-yaml-tags"] !== false) {
host.WriteFile("code-model-v4.yaml", serialize(result, codeModelSchema), undefined, "code-model-v4");
}
if (options["emit-yaml-tags"] !== true) {
host.WriteFile("code-model-v4-no-tags.yaml", serialize(result), undefined, "code-model-v4-no-tags");
}
} catch (E) {
if (debug) {
console.error(`${__filename} - FAILURE ${JSON.stringify(E)} ${E.stack}`);
}
throw E;
}
}

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

@ -0,0 +1,14 @@
import { CodeModel } from "@azure-tools/codemodel";
import { Session } from "@azure-tools/autorest-extension-base";
export class Example {
codeModel: CodeModel;
constructor(protected session: Session<CodeModel>) {
this.codeModel = session.model; // shadow(session.model, filename);
}
process() {
return this.codeModel;
}
}

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

@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { serialize } from "@azure-tools/codegen";
import { Host, startSession } from "@azure-tools/autorest-extension-base";
import { codeModelSchema, CodeModel } from "@azure-tools/codemodel";
import { Example } from "./example";
export async function processRequest(host: Host) {
const debug = (await host.GetValue("debug")) || false;
try {
const session = await startSession<CodeModel>(host, {}, codeModelSchema);
// process
const plugin = new Example(session);
// go!
const result = plugin.process();
// output the model to the pipeline
host.WriteFile("code-model-v4.yaml", serialize(result, codeModelSchema), undefined, "code-model-v4");
host.WriteFile("code-model-v4-no-tags.yaml", serialize(result), undefined, "code-model-v4-no-tags");
} catch (E) {
if (debug) {
console.error(`${__filename} - FAILURE ${JSON.stringify(E)} ${E.stack}`);
}
throw E;
}
}

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

@ -0,0 +1,299 @@
import {
CodeModel,
Schema,
ObjectSchema,
isObjectSchema,
SchemaType,
Property,
ParameterLocation,
Operation,
Parameter,
VirtualParameter,
getAllProperties,
ImplementationLocation,
Request,
} from "@azure-tools/codemodel";
import { Session } from "@azure-tools/autorest-extension-base";
import { values, items, length, Dictionary, refCount, clone } from "@azure-tools/linq";
import { ModelerFourOptions } from "../modeler/modelerfour-options";
const xmsThreshold = "x-ms-payload-flattening-threshold";
const xmsFlatten = "x-ms-client-flatten";
const isCurrentlyFlattening = "x-ms-flattening";
const hasBeenFlattened = "x-ms-flattened";
export class Flattener {
codeModel: CodeModel;
options: ModelerFourOptions = {};
threshold = 0;
recursePayload = false;
constructor(protected session: Session<CodeModel>) {
this.codeModel = session.model; // shadow(session.model, filename);
}
async init() {
// get our configuration for this run.
this.options = await this.session.getValue("modelerfour", {});
this.threshold = await this.session.getValue("payload-flattening-threshold", 0);
this.recursePayload = await this.session.getValue("recursive-payload-flattening", false);
return this;
}
*getFlattenedParameters(
parameter: Parameter,
property: Property,
path: Array<Property> = [],
): Iterable<VirtualParameter> {
if (property.readOnly) {
// skip read-only properties
return;
}
if (isObjectSchema(property.schema) && this.recursePayload === true) {
for (const child of getAllProperties(<ObjectSchema>property.schema)) {
yield* this.getFlattenedParameters(parameter, child, [...path, property]);
}
} else {
const vp = new VirtualParameter(
property.language.default.name,
property.language.default.description,
property.schema,
{
...property,
implementation: ImplementationLocation.Method,
originalParameter: parameter,
targetProperty: property,
pathToProperty: path,
},
);
delete (<any>vp).serializedName;
delete (<any>vp).readOnly;
delete (<any>vp).isDiscriminator;
delete (<any>vp).flattenedNames;
// if the parameter has "x-ms-parameter-grouping" extension, (and this is a top level parameter) then we should copy that to the vp.
if (path.length === 0 && parameter.extensions?.["x-ms-parameter-grouping"]) {
(vp.extensions = vp.extensions || {})["x-ms-parameter-grouping"] =
parameter.extensions?.["x-ms-parameter-grouping"];
}
yield vp;
}
// ·
}
/**
* This flattens an request's parameters (ie, takes the parameters from an operation and if they are objects will attempt to create inline versions of them)
*/
flattenPayload(request: Request, parameter: Parameter, schema: ObjectSchema) {
// hide the original parameter
parameter.flattened = true;
for (const property of values(getAllProperties(schema))) {
if (property.readOnly) {
// skip read-only properties
continue;
}
for (const vp of this.getFlattenedParameters(parameter, property)) {
request.parameters?.push(vp);
}
}
}
/**
* This will flatten models that are marked 'x-ms-client-flatten'
* @param schema schema to recursively flatten
*/
flattenSchema(schema: ObjectSchema) {
const state = schema.extensions?.[isCurrentlyFlattening];
if (state === false) {
// already done.
return;
}
if (state === true) {
// in progress.
throw new Error(
`Circular reference encountered during processing of x-ms-client flatten ('${schema.language.default.name}')`,
);
}
// hasn't started yet.
schema.extensions = schema.extensions || {};
schema.extensions[isCurrentlyFlattening] = true;
// ensure that parent schemas are done first -- this should remove
// the problem when the order isn't just right.
for (const parent of values(schema.parents?.immediate)) {
if (isObjectSchema(parent)) {
this.flattenSchema(parent);
}
}
if (schema.properties) {
for (const { key: index, value: property } of items(schema.properties).toArray().reverse()) {
if (isObjectSchema(property.schema) && property.extensions?.[xmsFlatten]) {
// first, ensure tha the child is pre-flattened
this.flattenSchema(property.schema);
// remove that property from the scheama
schema.properties.splice(index, 1);
// copy all of the properties from the child into this
// schema
for (const childProperty of values(getAllProperties(property.schema))) {
schema.addProperty(
new Property(
childProperty.language.default.name,
childProperty.language.default.description,
childProperty.schema,
{
...(<any>childProperty),
flattenedNames: [
property.serializedName,
...(childProperty.flattenedNames ? childProperty.flattenedNames : [childProperty.serializedName]),
],
required: property.required && childProperty.required,
},
),
);
}
// remove the extension
delete property.extensions[xmsFlatten];
if (length(property.extensions) === 0) {
delete property["extensions"];
}
// and mark the child class as 'do-not-generate' ?
(property.schema.extensions = property.schema.extensions || {})[hasBeenFlattened] = true;
}
}
}
schema.extensions[isCurrentlyFlattening] = false;
}
process() {
// support 'x-ms-payload-flattening-threshold' per-operation
// support '--payload-flattening-threshold:X' global setting
if (this.options["flatten-models"] === true) {
for (const schema of values(this.codeModel.schemas.objects)) {
this.flattenSchema(schema);
}
if (!this.options["keep-unused-flattened-models"]) {
let dirty = false;
do {
// reset on every pass
dirty = false;
// remove unreferenced models
for (const { key, value: schema } of items(this.codeModel.schemas.objects).toArray()) {
// only remove unreferenced models that have been flattened.
if (!schema.extensions?.[hasBeenFlattened]) {
continue;
}
if (schema.discriminatorValue || schema.discriminator) {
// it's polymorphic -- I don't think we can remove this
continue;
}
if (schema.children?.all || schema.parents?.all) {
// it's got either a parent or child schema.
continue;
}
if (refCount(this.codeModel, schema) === 1) {
this.codeModel.schemas.objects?.splice(key, 1);
dirty = true;
break;
}
}
} while (dirty);
}
for (const schema of values(this.codeModel.schemas.objects)) {
if (schema.extensions) {
delete schema.extensions[isCurrentlyFlattening];
// don't want this until I have removed the unreferenced models.
// delete schema.extensions[hasBeenFlattened];
if (length(schema.extensions) === 0) {
delete schema["extensions"];
}
}
}
}
if (this.options["flatten-payloads"] === true) {
/**
* BodyParameter Payload Flattening
*
* A body parameter is flattened (one level) when:
*
* - the body parameter schema is an object
* - the body parameter schema is not polymorphic (is this true?)
*
*
*
* and one of:
* - the body parameter has x-ms-client-flatten: true
* - the operation has x-ms-payload-flattening-threshold greater than zero and the property count in the body parameter is lessthan or equal to that.
* - the global configuration option payload-flattening-threshold is greater than zero and the property count in the body parameter is lessthan or equal to that
*
*/
// flatten payloads
for (const group of this.codeModel.operationGroups) {
for (const operation of group.operations) {
// when there are multiple requests in an operation
// and the generator asks not to flatten them
if (length(operation.requests) > 1 && this.options["multiple-request-parameter-flattening"] === false) {
continue;
}
for (const request of values(operation.requests)) {
const body = values(request.parameters).first(
(p) =>
p.protocol.http?.in === ParameterLocation.Body && p.implementation === ImplementationLocation.Method,
);
if (body && isObjectSchema(body.schema)) {
const schema = <ObjectSchema>body.schema;
if (schema.discriminator) {
// skip flattening on polymorphic payloads, since you don't know the actual type.
continue;
}
let flattenOperationPayload = body?.extensions?.[xmsFlatten];
if (flattenOperationPayload === false) {
// told not to explicitly.
continue;
}
if (!flattenOperationPayload) {
const threshold = <number>operation.extensions?.[xmsThreshold] ?? this.threshold;
if (threshold > 0) {
// get the count of the (non-readonly) properties in the schema
flattenOperationPayload =
length(
values(getAllProperties(schema)).where(
(property) => property.readOnly !== true && property.schema.type !== SchemaType.Constant,
),
) <= threshold;
}
}
if (flattenOperationPayload) {
this.flattenPayload(request, body, schema);
request.updateSignatureParameters();
}
}
}
}
}
}
return this.codeModel;
}
}

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

@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { serialize } from "@azure-tools/codegen";
import { Host, startSession } from "@azure-tools/autorest-extension-base";
import { codeModelSchema, CodeModel } from "@azure-tools/codemodel";
import { Flattener } from "./flattener";
export async function processRequest(host: Host) {
const debug = (await host.GetValue("debug")) || false;
try {
const session = await startSession<CodeModel>(host, {}, codeModelSchema);
const options = <any>await session.getValue("modelerfour", {});
// process
const plugin = await new Flattener(session).init();
// go!
const result = plugin.process();
// output the model to the pipeline
if (options["emit-yaml-tags"] !== false) {
host.WriteFile("code-model-v4.yaml", serialize(result, codeModelSchema), undefined, "code-model-v4");
}
if (options["emit-yaml-tags"] !== true) {
host.WriteFile("code-model-v4-no-tags.yaml", serialize(result), undefined, "code-model-v4-no-tags");
}
} catch (E) {
if (debug) {
console.error(`${__filename} - FAILURE ${JSON.stringify(E)} ${E.stack}`);
}
throw E;
}
}

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

@ -0,0 +1,162 @@
import {
CodeModel,
Schema,
GroupSchema,
isObjectSchema,
SchemaType,
GroupProperty,
ParameterLocation,
Operation,
Parameter,
VirtualParameter,
getAllProperties,
ImplementationLocation,
OperationGroup,
Request,
SchemaContext,
} from "@azure-tools/codemodel";
import { Session } from "@azure-tools/autorest-extension-base";
import { values, items, length, Dictionary, refCount, clone } from "@azure-tools/linq";
import { pascalCase, camelCase } from "@azure-tools/codegen";
import { ModelerFourOptions } from "../modeler/modelerfour-options";
const mergeReponseHeaders = "merge-response-headers";
const xmsParameterGrouping = "x-ms-parameter-grouping";
export class Grouper {
codeModel: CodeModel;
options: ModelerFourOptions = {};
groups: Dictionary<GroupSchema> = {};
constructor(protected session: Session<CodeModel>) {
this.codeModel = session.model; // shadow(session.model, filename);
}
async init() {
// get our configuration for this run.
this.options = await this.session.getValue("modelerfour", {});
return this;
}
process() {
if (this.options["group-parameters"] === true) {
for (const group of this.codeModel.operationGroups) {
for (const operation of group.operations) {
for (const request of values(operation.requests)) {
this.processParameterGroup(group, operation, request);
request.updateSignatureParameters();
}
operation.updateSignatureParameters();
}
}
}
/*
if (this.options[mergeReponseHeaders] === true) {
for (const group of this.codeModel.operationGroups) {
for (const operation of group.operations) {
this.processResponseHeaders(operation);
operation.request.updateSignatureParameters();
}
}
}
*/
return this.codeModel;
}
proposedName(group: OperationGroup, operation: Operation, parameter: Parameter) {
const xmsp = parameter.extensions?.[xmsParameterGrouping];
if (xmsp.name && typeof xmsp.name === "string") {
return xmsp.name;
}
const postfix = xmsp.postfix && typeof xmsp.postfix === "string" ? xmsp.postfix : "Parameters";
return pascalCase(`${group.$key} ${operation.language.default.name} ${postfix}`);
}
processParameterGroup(group: OperationGroup, operation: Operation, request: Request) {
const grouped = [
...values(operation.parameters)
.concat(values(request.parameters))
.where(
(parameter) =>
parameter.extensions?.[xmsParameterGrouping] &&
parameter.schema.type !== SchemaType.Constant &&
parameter.implementation !== ImplementationLocation.Client,
),
];
if (grouped.length > 0) {
// create a parameter group object schema for the selected parameters.
const addedGroupedParameters = new Map<GroupSchema, Parameter>();
for (const parameter of grouped) {
const groupName = this.proposedName(group, operation, parameter);
// see if we've started the schema for this yet.
if (!this.groups[groupName]) {
// create a new object schema for this group
const schema = new GroupSchema(groupName, "Parameter group");
schema.usage = [SchemaContext.Input];
this.groups[groupName] = schema;
this.codeModel.schemas.add(schema);
}
const schema = this.groups[groupName];
// see if the group has this parameter.
const existingProperty = values(schema.properties).first(
(each) => each.language.default.name === parameter.language.default.name,
);
if (existingProperty) {
// we have a property by this name one already
// mark the groupproperty with this parameter (so we can find it if needed)
existingProperty.originalParameter.push(parameter);
} else {
// create a property for this parameter.
const gp = new GroupProperty(
parameter.language.default.name,
parameter.language.default.description,
parameter.schema,
{
required: parameter.required,
},
);
gp.originalParameter.push(parameter);
schema.add(gp);
}
// check if this groupSchema has been added as a parameter for this operation yet.
if (!addedGroupedParameters.has(schema)) {
addedGroupedParameters.set(
schema,
request.addParameter(
new Parameter(camelCase(schema.language.default.name), schema.language.default.description, schema, {
implementation: ImplementationLocation.Method,
}),
),
);
}
// make sure that it's not optional if any parameter are not optional.
const pp = <Parameter>addedGroupedParameters.get(schema);
pp.required = pp.required || parameter.required;
// mark the original parameter with the target of the grouping.
parameter.groupedBy = pp;
// remove the grouping extension from the original parameter.
if (parameter.extensions) {
delete parameter.extensions[xmsParameterGrouping];
if (length(parameter.extensions) === 0) {
delete parameter["extensions"];
}
}
}
}
}
processResponseHeaders(operation: Operation) {
throw new Error("Method not implemented.");
}
}

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

@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { serialize } from "@azure-tools/codegen";
import { Host, startSession } from "@azure-tools/autorest-extension-base";
import { codeModelSchema, CodeModel } from "@azure-tools/codemodel";
import { Grouper } from "./grouper";
export async function processRequest(host: Host) {
const debug = (await host.GetValue("debug")) || false;
try {
const session = await startSession<CodeModel>(host, {}, codeModelSchema);
const options = <any>await session.getValue("modelerfour", {});
// process
const plugin = await new Grouper(session).init();
// go!
const result = plugin.process();
// output the model to the pipeline
if (options["emit-yaml-tags"] !== false) {
host.WriteFile("code-model-v4.yaml", serialize(result, codeModelSchema), undefined, "code-model-v4");
}
if (options["emit-yaml-tags"] !== true) {
host.WriteFile("code-model-v4-no-tags.yaml", serialize(result), undefined, "code-model-v4-no-tags");
}
} catch (E) {
if (debug) {
console.error(`${__filename} - FAILURE ${JSON.stringify(E)} ${E.stack}`);
}
throw E;
}
}

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

@ -0,0 +1,24 @@
import { AutoRestExtension } from "@azure-tools/autorest-extension-base";
import { processRequest as modelerfour } from "./modeler/plugin-modelerfour";
import { processRequest as preNamer } from "./prenamer/plugin-prenamer";
import { processRequest as flattener } from "./flattener/plugin-flattener";
import { processRequest as grouper } from "./grouper/plugin-grouper";
import { processRequest as checker } from "./checker/plugin-checker";
import { processRequest as prechecker } from "./quality-precheck/prechecker";
export async function initializePlugins(pluginHost: AutoRestExtension) {
pluginHost.Add("prechecker", prechecker);
pluginHost.Add("modelerfour", modelerfour);
pluginHost.Add("grouper", grouper);
pluginHost.Add("pre-namer", preNamer);
pluginHost.Add("flattener", flattener);
pluginHost.Add("checker", checker);
}
async function main() {
const pluginHost = new AutoRestExtension();
await initializePlugins(pluginHost);
await pluginHost.Run();
}
main();

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

@ -0,0 +1,427 @@
import { Session } from "@azure-tools/autorest-extension-base";
import * as OpenAPI from "@azure-tools/openapi";
import { values, length, items, ToDictionary, Dictionary } from "@azure-tools/linq";
import {
ChoiceSchema,
XmlSerlializationFormat,
ExternalDocumentation,
ApiVersion,
Deprecation,
ChoiceValue,
SetType,
} from "@azure-tools/codemodel";
import { StringFormat, JsonType, ParameterLocation } from "@azure-tools/openapi";
import { getPascalIdentifier } from "@azure-tools/codegen";
export interface XMSEnum {
modelAsString?: boolean;
values: [{ value: any; description?: string; name?: string }];
name: string;
}
const removeKnownParameters = [
"x-ms-metadata",
"x-ms-enum",
"x-ms-code-generation-settings",
"x-ms-client-name",
"x-ms-parameter-location",
"x-ms-original",
"x-ms-requestBody-name",
"x-ms-requestBody-index",
"x-ms-api-version",
"x-ms-text",
];
// ref: https://www.w3schools.com/charsets/ref_html_ascii.asp
const specialCharacterMapping: { [character: string]: string } = {
"!": "exclamation mark",
'"': "quotation mark",
"#": "number sign",
"$": "dollar sign",
"%": "percent sign",
"&": "ampersand",
"'": "apostrophe",
"(": "left parenthesis",
")": "right parenthesis",
"*": "asterisk",
"+": "plus sign",
",": "comma",
"-": "hyphen",
".": "period",
"/": "slash",
":": "colon",
";": "semicolon",
"<": "less-than",
"=": "equals-to",
">": "greater-than",
"?": "question mark",
"@": "at sign",
"[": "left square bracket",
"\\": "backslash",
"]": "right square bracket",
"^": "caret",
"_": "underscore",
"`": "grave accent",
"{": "left curly brace",
"|": "vertical bar",
"}": "right curly brace",
"~": "tilde",
};
const apiVersionParameterNames = ["api-version", "apiversion", "x-ms-api-version", "x-ms-version"];
export function getValidEnumValueName(originalString: string): string {
if (typeof originalString === "string") {
return !originalString.match(/[A-Za-z0-9]/g)
? getPascalIdentifier(
originalString
.split("")
.map((x) => specialCharacterMapping[x])
.join(" "),
)
: originalString;
}
return originalString;
}
export class Interpretations {
isTrue(value: any) {
return value === true || value === "true" || value === "True" || value === "TRUE";
}
getConstantValue(schema: OpenAPI.Schema, value: any) {
switch (schema.type) {
case JsonType.String:
switch (schema.format) {
// member should be byte array
// on wire format should be base64url
case StringFormat.Base64Url:
// return this.parseBase64UrlValue(value);
return value;
case StringFormat.Byte:
case StringFormat.Certificate:
// return this.parseByteArrayValue(value);
return value;
case StringFormat.Char:
// a single character
return `${value}`.charAt(0);
case StringFormat.Date:
// return this.parseDateValue(value);
return value;
case StringFormat.DateTime:
// return this.parseDateTimeValue(value);
return value;
case StringFormat.DateTimeRfc1123:
// return this.parseDateTimeRfc1123Value(value);
return value;
case StringFormat.Duration:
// return this.parseDurationValue(value);
return value;
case StringFormat.Uuid:
return value;
case StringFormat.Url:
return value;
case StringFormat.Password:
throw new Error("Constant values for String/Passwords should never be in input documents");
case StringFormat.OData:
return value;
case StringFormat.None:
case undefined:
case null:
return value;
default:
// console.error(`String schema '${name}' with unknown format: '${schema.format}' is treated as simple string.`);
throw new Error(
`Unknown type for constant value for String '${schema.format}'--cannot create constant value.`,
);
}
case JsonType.Boolean:
return this.isTrue(value);
case JsonType.Number:
return Number.parseFloat(value);
case JsonType.Integer:
return Number.parseInt(value);
}
}
isApiVersionParameter(parameter: OpenAPI.Parameter): boolean {
// Always let x-ms-api-version override the check
if (parameter["x-ms-api-version"] !== undefined) {
return !!parameter["x-ms-api-version"] === true;
}
// It's an api-version parameter if it's a query param with an expected name
return (
parameter.in === ParameterLocation.Query &&
!!apiVersionParameterNames.find((each) => each === parameter.name.toLowerCase())
);
}
getEnumChoices(schema: OpenAPI.Schema): Array<ChoiceValue> {
if (schema && schema.enum) {
const xmse = <XMSEnum>schema["x-ms-enum"];
return xmse && xmse.values
? xmse.values.map((each) => {
const name = getValidEnumValueName(each.name !== undefined ? each.name : each.value);
const value = this.getConstantValue(schema, each.value);
return new ChoiceValue(`${name}`, each.description || ``, value);
})
: schema.enum.map((each) => {
const name = getValidEnumValueName(each);
const value = this.getConstantValue(schema, each);
return new ChoiceValue(`${name}`, ``, value);
});
}
return [];
}
isEmptyObject(schema: OpenAPI.Schema): boolean {
const hasAdditionalProps =
typeof schema.additionalProperties === "boolean"
? schema.additionalProperties
: length(schema.additionalProperties) !== 0;
return (
schema.type === JsonType.Object &&
length(schema.allOf) + length(schema.anyOf) + length(schema.oneOf) + length(schema.properties) === 0 &&
!hasAdditionalProps &&
!schema.discriminator
);
}
getSerialization(schema: OpenAPI.Schema): any | undefined {
const xml = this.getXmlSerialization(schema);
if (xml) {
return {
xml,
};
}
return undefined;
}
getXmlSerialization(schema: OpenAPI.Schema): XmlSerlializationFormat | undefined {
if (schema.xml) {
if (schema.xml["x-ms-text"] && schema.xml.attribute) {
throw new Error(`XML serialization for a schema cannot be in both 'text' and 'attribute'`);
}
return {
attribute: schema.xml.attribute || false,
wrapped: schema.xml.wrapped || false,
text: schema.xml["x-ms-text"] || false,
name: schema.xml.name || undefined,
namespace: schema.xml.namespace || undefined,
prefix: schema.xml.prefix || undefined,
extensions: this.getExtensionProperties(schema.xml),
};
}
return undefined;
}
getExternalDocs(schema: OpenAPI.Schema): ExternalDocumentation | undefined {
return undefined;
}
getExample(schema: OpenAPI.Schema): any {
return undefined;
}
getApiVersions(schema: OpenAPI.Schema | OpenAPI.HttpOperation | OpenAPI.PathItem): Array<ApiVersion> | undefined {
if (schema["x-ms-metadata"] && schema["x-ms-metadata"]["apiVersions"]) {
const v = values(<Array<string>>schema["x-ms-metadata"]["apiVersions"])
.select((each) =>
SetType(ApiVersion, {
version: each.replace(/^-/, "").replace(/\+$/, ""),
range: each.startsWith("-") ? <any>"-" : each.endsWith("+") ? "+" : undefined,
}),
)
.toArray();
return v;
}
return undefined;
}
getApiVersionValues(node: OpenAPI.Schema | OpenAPI.HttpOperation | OpenAPI.PathItem): Array<string> {
if (node["x-ms-metadata"] && node["x-ms-metadata"]["apiVersions"]) {
return values(<Array<string>>node["x-ms-metadata"]["apiVersions"])
.distinct()
.toArray();
}
return [];
}
getDeprecation(schema: OpenAPI.Schema): Deprecation | undefined {
if (schema.deprecated) {
// todo
}
return undefined;
}
constructor(private session: Session<OpenAPI.Model>) {}
xmsMeta(obj: any, key: string) {
const m = obj["x-ms-metadata"];
return m ? m[key] : undefined;
}
xmsMetaFallback(obj: any, obj2: any, key: string) {
return this.xmsMeta(obj, key) || this.xmsMeta(obj2, key);
}
splitOpId(opId: string) {
const p = opId.indexOf("_");
return p != -1
? {
group: opId.substr(0, p),
member: opId.substr(p + 1),
}
: {
group: "",
member: opId,
};
}
isBinarySchema(schema: OpenAPI.Schema | undefined) {
return !!(
schema &&
(schema.format === StringFormat.Binary || schema.format === "file" || <any>schema.type === "file")
);
}
getOperationId(httpMethod: string, path: string, original: OpenAPI.HttpOperation) {
if (original.operationId) {
return this.splitOpId(original.operationId);
}
// synthesize from tags.
if (original.tags && length(original.tags) > 0) {
const newOperationId = length(original.tags) === 1
? `${original.tags[0]}`
: `${original.tags[0]}_${original.tags[1]}`;
this.session.warning(
`Generating 'operationId' to '${newOperationId}' for '${httpMethod}' operation on path '${path}' `,
["Interpretations"],
original,
);
return this.splitOpId(newOperationId);
}
this.session.error(
`NEED 'operationId' for '${httpMethod}' operation on path '${path}' `,
["Interpretations"],
original,
);
return this.splitOpId("unknown-method");
}
getDescription(
defaultValue: string,
original: OpenAPI.Extensions & { title?: string; summary?: string; description?: string },
): string {
if (original) {
return original.description || original.title || original.summary || defaultValue;
}
return defaultValue;
}
getPreferredName(original: any, preferredName?: string, fallbackName?: string) {
return (
original["x-ms-client-name"] ??
preferredName ??
original?.["x-ms-metadata"]?.["name"] ??
fallbackName ??
original["name"] ??
"MISSING_NAME"
);
}
getName(defaultValue: string, original: any): string {
return original["x-ms-client-name"] ?? original?.["x-ms-metadata"]?.["name"] ?? defaultValue;
}
/** gets the operation path from metadata, falls back to the OAI3 path key */
getPath(pathItem: OpenAPI.PathItem, operation: OpenAPI.HttpOperation, path: string) {
return this.xmsMeta(pathItem, "path") || this.xmsMeta(operation, "path") || path;
}
/*
/** creates server entries that are kept in the codeModel.protocol.http, and then referenced in each operation
*
* @note - this is where deduplication of server entries happens.
* /
getServers(operation: OpenAPI.HttpOperation): Array<HttpServer> {
return values(operation.servers).select(server => {
const p = <HttpModel>this.codeModel.protocol.http;
const f = p && p.servers.find(each => each.url === server.url);
if (f) {
return f;
}
const s = new HttpServer(server.url, this.getDescription('MISSING-SERVER-DESCRIPTION', server));
if (server.variables && length(server.variables) > 0) {
s.variables = items(server.variables).where(each => !!each.key).select(each => {
const description = this.getDescription('MISSING-SERVER_VARIABLE-DESCRIPTION', each.value);
const variable = each.value;
const schema = variable.enum ?
this.getEnumSchemaForVarible(each.key, variable) :
this.codeModel.schemas.add(new StringSchema(`ServerVariable/${each.key}`, description));
const serverVariable = new ServerVariable(
each.key,
this.getDescription('MISSING-SERVER_VARIABLE-DESCRIPTION', variable),
schema,
{
default: variable.default,
// required: TODO: implement required on server variables
});
return serverVariable;
}).toArray();
}
(<HttpModel>this.codeModel.protocol.http).servers.push(s);
return s;
}).toArray();
}
*/
getEnumSchemaForVarible(name: string, somethingWithEnum: { enum?: Array<string> }): ChoiceSchema {
return new ChoiceSchema(name, this.getDescription("MISSING-SERVER-VARIABLE-ENUM-DESCRIPTION", somethingWithEnum));
}
getExtensionProperties(dictionary: Dictionary<any>, additional?: Dictionary<any>): Dictionary<any> | undefined {
const main = Interpretations.getExtensionProperties(dictionary);
if (additional) {
const more = Interpretations.getExtensionProperties(additional);
if (more) {
return { ...main, ...more };
}
}
return main;
}
getClientDefault(dictionary: Dictionary<any>, additional?: Dictionary<any>): string | number | boolean | undefined {
return dictionary?.["x-ms-client-default"] || additional?.["x-ms-client-default"] || undefined;
}
static getExtensionProperties(dictionary: Dictionary<any>): Dictionary<any> | undefined {
const result = ToDictionary(OpenAPI.includeXDash(dictionary), (each) => dictionary[each]);
for (const each of removeKnownParameters) {
delete result[each];
}
return length(result) === 0 ? undefined : result;
}
}

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

@ -0,0 +1,61 @@
/**
* List of configuration that can be used with modelerfour.
*/
export interface ModelerFourOptions {
/**
* Flag to automatically add the Content-Type header to operations.
*/
"always-create-content-type-parameter"?: boolean;
/**
* Flag to automatically add the Accept header to operations.
*/
"always-create-accept-parameter"?: boolean;
"always-seal-x-ms-enums"?: boolean;
"flatten-models"?: boolean;
"flatten-payloads"?: boolean;
"keep-unused-flattened-models"?: boolean;
"multiple-request-parameter-flattening"?: boolean;
"group-parameters"?: boolean;
"additional-checks"?: boolean;
"lenient-model-deduplication"?: boolean;
"naming"?: ModelerFourNamingOptions;
"prenamer"?: boolean;
"resolve-schema-name-collisons"?: boolean;
/**
* In the case where a type only definition is to inherit another type remove it.
* @example ChildSchema: {allOf: [ParentSchema]}.
* In this case ChildSchema will be removed and all reference to it will be updated to point to ParentSchema
*/
"remove-empty-child-schemas"?: boolean;
}
export interface ModelerFourNamingOptions {
"preserve-uppercase-max-length"?: number;
"parameter"?: string;
"property"?: string;
"operation"?: string;
"operationGroup"?: string;
"header"?: string;
"choice"?: string;
"choiceValue"?: string;
"constant"?: string;
"constantParameter"?: string;
"client"?: string;
"type"?: string;
"global"?: string;
"local"?: string;
"override"?: any;
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { deserialize, serialize } from "@azure-tools/codegen";
import { Host, startSession } from "@azure-tools/autorest-extension-base";
import * as OpenAPI from "@azure-tools/openapi";
import { ModelerFour } from "./modelerfour";
import { codeModelSchema, CodeModel } from "@azure-tools/codemodel";
export async function processRequest(host: Host) {
const debug = (await host.GetValue("debug")) || false;
try {
const session = await startSession<OpenAPI.Model>(host, undefined, undefined, "prechecked-openapi-document");
const options = <any>await session.getValue("modelerfour", {});
// process
const modeler = await new ModelerFour(session).init();
// go!
const codeModel = modeler.process();
// throw on errors.
if (!(await session.getValue("ignore-errors", false))) {
session.checkpoint();
}
// output the model to the pipeline
if (options["emit-yaml-tags"] !== false) {
host.WriteFile("code-model-v4.yaml", serialize(codeModel, codeModelSchema), undefined, "code-model-v4");
}
if (options["emit-yaml-tags"] !== true) {
host.WriteFile("code-model-v4-no-tags.yaml", serialize(codeModel), undefined, "code-model-v4-no-tags");
}
} catch (E) {
if (debug) {
console.error(`${__filename} - FAILURE ${JSON.stringify(E)} ${E.stack}`);
}
throw E;
}
}

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

@ -0,0 +1,94 @@
import { Languages } from "@azure-tools/codemodel";
import { length, Dictionary } from "@azure-tools/linq";
import { removeSequentialDuplicates, fixLeadingNumber, deconstruct, Style, Styler } from "@azure-tools/codegen";
export function getNameOptions(typeName: string, components: Array<string>) {
const result = new Set<string>();
// add a variant for each incrementally inclusive parent naming scheme.
for (let i = 0; i < length(components); i++) {
const subset = Style.pascal([...removeSequentialDuplicates(components.slice(-1 * i, length(components)))]);
result.add(subset);
}
// add a second-to-last-ditch option as <typename>.<name>
result.add(
Style.pascal([
...removeSequentialDuplicates([...fixLeadingNumber(deconstruct(typeName)), ...deconstruct(components.last)]),
]),
);
return [...result.values()];
}
interface SetNameOptions {
/**
* Remove consecutive duplicate words in the name.
* @example "FooBarBarSomething" -> "FooBarSomething"
*/
removeDuplicates?: boolean;
/**
* Set containing the list of names already used in the given scope.
*/
existingNames?: Set<string>;
/**
* If it should allow duplicate models.(Later in the pipeline duplicate models will be deduplicated.)
*/
lenientModelDeduplication?: boolean;
}
const setNameDefaultOptions: SetNameOptions = Object.freeze({
removeDuplicates: true,
});
export function setName(
thing: { language: Languages },
styler: Styler,
defaultValue: string,
overrides: Dictionary<string>,
options?: SetNameOptions,
) {
setNameAllowEmpty(thing, styler, defaultValue, overrides, options);
if (!thing.language.default.name) {
throw new Error("Name is empty!");
}
}
export function setNameAllowEmpty(
thing: { language: Languages },
styler: Styler,
defaultValue: string,
overrides: Dictionary<string>,
options?: SetNameOptions,
) {
options = { ...setNameDefaultOptions, ...options };
const initialName =
defaultValue && isUnassigned(thing.language.default.name) ? defaultValue : thing.language.default.name;
const namingOptions = [
...(options.removeDuplicates ? [styler(initialName, true, overrides)] : []),
styler(initialName, false, overrides),
];
for (const newName of namingOptions) {
// Check if the new name is not yet taken or lenientModelDeduplication is enabled then we don't care about duplicates.
if (newName && (!options.existingNames?.has(newName) || options.lenientModelDeduplication)) {
options.existingNames?.add(newName);
thing.language.default.name = newName;
return;
}
}
if (initialName != "") {
const namingOptionsStr = namingOptions.join(",");
throw new Error(
`Couldn't style name '${initialName}'. All of the following naming possibilities created duplicate names: [${namingOptionsStr}]. You can try using 'modelerfour.lenient-model-deduplication' to allow such duplicates.`,
);
}
}
export function isUnassigned(value: string) {
return !value || value.indexOf("·") > -1;
}

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

@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { serialize } from "@azure-tools/codegen";
import { Host, startSession } from "@azure-tools/autorest-extension-base";
import { codeModelSchema, CodeModel } from "@azure-tools/codemodel";
import { PreNamer } from "./prenamer";
export async function processRequest(host: Host) {
const debug = (await host.GetValue("debug")) || false;
try {
const session = await startSession<CodeModel>(host, {}, codeModelSchema);
const options = <any>await session.getValue("modelerfour", {});
// process
const plugin = await new PreNamer(session).init();
// go!
const result = plugin.process();
// output the model to the pipeline
if (options["emit-yaml-tags"] !== false) {
host.WriteFile("code-model-v4.yaml", serialize(result, codeModelSchema), undefined, "code-model-v4");
}
if (options["emit-yaml-tags"] !== true) {
host.WriteFile("code-model-v4-no-tags.yaml", serialize(result), undefined, "code-model-v4-no-tags");
}
} catch (E) {
if (debug) {
console.error(`${__filename} - FAILURE ${JSON.stringify(E)} ${E.stack}`);
}
throw E;
}
}

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

@ -0,0 +1,411 @@
import {
CodeModel,
Parameter,
isVirtualParameter,
ObjectSchema,
isObjectSchema,
getAllParentProperties,
Languages,
SchemaType,
Schema,
ImplementationLocation,
Operation,
Request,
Response,
ChoiceSchema,
StringSchema,
SealedChoiceSchema,
PrimitiveSchema,
} from "@azure-tools/codemodel";
import { Session } from "@azure-tools/autorest-extension-base";
import { values, length, Dictionary, items } from "@azure-tools/linq";
import { selectName, Style, Styler } from "@azure-tools/codegen";
import { ModelerFourOptions } from "../modeler/modelerfour-options";
import { getNameOptions, isUnassigned, setName, setNameAllowEmpty } from "./naming-utils";
/*
* This function checks the `schemaNames` set for a proposed name for the
* given `schema` using the `indexer` to generate the key to the set. A
* custom `indexer` would be used when there's a piece of information other
* than the name itself to determine the uniqueness of the name (like a
* namespace).
*/
function deduplicateSchemaName(
schema: Schema,
schemaNames: Set<string>,
session: Session<CodeModel>,
indexer: (schema: Schema, newName: string) => string = (schema: Schema, proposedName: string) => proposedName,
): void {
const schemaName = schema.language.default.name;
const maxDedupes = 1000;
if (schemaNames.has(indexer(schema, schemaName))) {
for (let i = 1; i <= maxDedupes; i++) {
const newName = `${schemaName}AutoGenerated${i === 1 ? "" : i}`;
if (!schemaNames.has(indexer(schema, newName))) {
schema.language.default.name = newName;
schemaNames.add(indexer(schema, newName));
session.warning(`Deduplicating schema name: '${schemaName}' -> '${newName}'`, ["PreNamer/DeduplicateName"]);
return;
}
}
session.error(
`Attempted to deduplicate schema name '${schema.language.default.name}' more than ${maxDedupes} times and failed.`,
["PreNamer/DeduplicateName"],
);
}
// We haven't seen the name before, add it
schemaNames.add(indexer(schema, schemaName));
}
export class PreNamer {
codeModel: CodeModel;
options: ModelerFourOptions = {};
format = {
parameter: Style.camel,
property: Style.camel,
operation: Style.pascal,
operationGroup: Style.pascal,
responseHeader: Style.pascal,
choice: Style.pascal,
choiceValue: Style.pascal,
constant: Style.pascal,
constantParameter: Style.camel,
type: Style.pascal,
client: Style.pascal,
local: Style.camel,
global: Style.pascal,
override: <Dictionary<string>>{},
};
enum = 0;
constant = 0;
constructor(protected session: Session<CodeModel>) {
this.codeModel = session.model; // shadow(session.model, filename);
}
async init() {
// get our configuration for this run.
this.options = await this.session.getValue("modelerfour", {});
const naming = this.options.naming || {};
const maxPreserve = Number(naming["preserve-uppercase-max-length"]) || 3;
this.format = {
parameter: Style.select(naming.parameter, Style.camel, maxPreserve),
property: Style.select(naming.property, Style.camel, maxPreserve),
operation: Style.select(naming.operation, Style.pascal, maxPreserve),
operationGroup: Style.select(naming.operationGroup, Style.pascal, maxPreserve),
responseHeader: Style.select(naming.header, Style.pascal, maxPreserve),
choice: Style.select(naming.choice, Style.pascal, maxPreserve),
choiceValue: Style.select(naming.choiceValue, Style.pascal, maxPreserve),
constant: Style.select(naming.constant, Style.pascal, maxPreserve),
constantParameter: Style.select(naming.constantParameter, Style.camel, maxPreserve),
client: Style.select(naming.client, Style.pascal, maxPreserve),
type: Style.select(naming.type, Style.pascal, maxPreserve),
local: Style.select(naming.local, Style.camel, maxPreserve),
global: Style.select(naming.global, Style.pascal, maxPreserve),
override: naming.override || {},
};
return this;
}
process() {
if (this.options["prenamer"] === false) {
return this.codeModel;
}
const deduplicateSchemaNames =
!!this.options["lenient-model-deduplication"] || !!this.options["resolve-schema-name-collisons"];
const existingNames = new Set<string>();
// choice
this.processChoiceNames(this.codeModel.schemas.choices, existingNames, deduplicateSchemaNames);
// sealed choice
this.processChoiceNames(this.codeModel.schemas.sealedChoices, existingNames, deduplicateSchemaNames);
// constant
for (const schema of values(this.codeModel.schemas.constants)) {
setName(schema, this.format.constant, `Constant${this.enum++}`, this.format.override);
}
// strings
for (const schema of values(this.codeModel.schemas.strings)) {
setName(schema, this.format.type, schema.type, this.format.override);
}
// number
for (const schema of values(this.codeModel.schemas.numbers)) {
setName(schema, this.format.type, schema.type, this.format.override);
}
for (const schema of values(this.codeModel.schemas.dates)) {
setName(schema, this.format.type, schema.type, this.format.override);
}
for (const schema of values(this.codeModel.schemas.dateTimes)) {
setName(schema, this.format.type, schema.type, this.format.override);
}
for (const schema of values(this.codeModel.schemas.durations)) {
setName(schema, this.format.type, schema.type, this.format.override);
}
for (const schema of values(this.codeModel.schemas.uuids)) {
setName(schema, this.format.type, schema.type, this.format.override);
}
for (const schema of values(this.codeModel.schemas.uris)) {
setName(schema, this.format.type, schema.type, this.format.override);
}
for (const schema of values(this.codeModel.schemas.unixtimes)) {
setName(schema, this.format.type, schema.type, this.format.override);
if (isUnassigned(schema.language.default.description)) {
schema.language.default.description = "date in seconds since 1970-01-01T00:00:00Z.";
}
}
for (const schema of values(this.codeModel.schemas.byteArrays)) {
setName(schema, this.format.type, schema.type, this.format.override);
}
for (const schema of values(this.codeModel.schemas.chars)) {
setName(schema, this.format.type, schema.type, this.format.override);
}
for (const schema of values(this.codeModel.schemas.booleans)) {
setName(schema, this.format.type, schema.type, this.format.override);
}
for (const schema of values(this.codeModel.schemas.flags)) {
setName(schema, this.format.type, schema.type, this.format.override);
}
// dictionary
for (const schema of values(this.codeModel.schemas.dictionaries)) {
setName(
schema,
this.format.type,
`DictionaryOf${schema.elementType.language.default.name}`,
this.format.override,
);
if (isUnassigned(schema.language.default.description)) {
schema.language.default.description = `Dictionary of ${schema.elementType.language.default.name}`;
}
}
for (const schema of values(this.codeModel.schemas.arrays)) {
setName(schema, this.format.type, `ArrayOf${schema.elementType.language.default.name}`, this.format.override);
if (isUnassigned(schema.language.default.description)) {
schema.language.default.description = `Array of ${schema.elementType.language.default.name}`;
}
}
const objectSchemaNames = new Set<string>();
for (const schema of values(this.codeModel.schemas.objects)) {
setName(schema, this.format.type, "", this.format.override, { existingNames, lenientModelDeduplication: this.options["lenient-model-deduplication"] });
if (deduplicateSchemaNames) {
deduplicateSchemaName(
schema,
objectSchemaNames,
this.session,
(schema: Schema, proposedName: string) => `${schema.language.default.namespace || ""}.${proposedName}`,
);
}
for (const property of values(schema.properties)) {
setName(property, this.format.property, "", this.format.override);
}
}
const groupSchemaNames = new Set<string>();
for (const schema of values(this.codeModel.schemas.groups)) {
setName(schema, this.format.type, "", this.format.override, {
existingNames,
lenientModelDeduplication: this.options["lenient-model-deduplication"],
});
if (deduplicateSchemaNames) {
deduplicateSchemaName(
schema,
groupSchemaNames,
this.session,
(schema: Schema, proposedName: string) => `${schema.language.default.namespace || ""}.${proposedName}`,
);
}
for (const property of values(schema.properties)) {
setName(property, this.format.property, "", this.format.override);
}
}
for (const parameter of values(this.codeModel.globalParameters)) {
if (parameter.schema.type === SchemaType.Constant) {
setName(parameter, this.format.constantParameter, "", this.format.override);
} else {
setName(parameter, this.format.parameter, "", this.format.override);
}
}
for (const operationGroup of this.codeModel.operationGroups) {
setNameAllowEmpty(operationGroup, this.format.operationGroup, operationGroup.$key, this.format.override, {
removeDuplicates: false,
});
for (const operation of operationGroup.operations) {
setName(operation, this.format.operation, "", this.format.override);
this.setParameterNames(operation);
for (const request of values(operation.requests)) {
this.setParameterNames(request);
}
for (const response of values(operation.responses)) {
this.setResponseHeaderNames(response);
}
for (const response of values(operation.exceptions)) {
this.setResponseHeaderNames(response);
}
const p = operation.language.default.paging;
if (p) {
p.group = p.group ? this.format.operationGroup(p.group, true, this.format.override) : undefined;
p.member = p.member ? this.format.operation(p.member, true, this.format.override) : undefined;
}
}
}
// set a styled client name
setName(this.codeModel, this.format.client, this.codeModel.info.title, this.format.override);
// fix collisions from flattening on ObjectSchemas
this.fixPropertyCollisions();
// fix collisions from flattening on VirtualParameters
this.fixParameterCollisions();
return this.codeModel;
}
private processChoiceNames(
choices: Array<ChoiceSchema | SealedChoiceSchema> | undefined,
existingNames: Set<string>,
deduplicateSchemaNames: boolean,
) {
const choiceSchemaNames = new Set<string>();
for (const schema of values(choices)) {
setName(schema, this.format.choice, `Enum${this.enum++}`, this.format.override, {
existingNames,
lenientModelDeduplication: this.options["lenient-model-deduplication"],
});
if (deduplicateSchemaNames) {
deduplicateSchemaName(schema, choiceSchemaNames, this.session);
}
for (const choice of values(schema.choices)) {
setName(choice, this.format.choiceValue, "", this.format.override, { removeDuplicates: false });
}
}
}
private setParameterNames(parameterContainer: Operation | Request) {
for (const parameter of values(parameterContainer.signatureParameters)) {
if (parameter.schema.type === SchemaType.Constant) {
setName(parameter, this.format.constantParameter, "", this.format.override);
} else {
setName(parameter, this.format.parameter, "", this.format.override);
}
}
for (const parameter of values(parameterContainer.parameters)) {
if ((parameterContainer.signatureParameters ?? []).indexOf(parameter) === -1) {
if (parameter.schema.type === SchemaType.Constant) {
setName(parameter, this.format.constantParameter, "", this.format.override);
} else {
if (parameter.implementation === ImplementationLocation.Client) {
setName(parameter, this.format.global, "", this.format.override);
} else {
setName(parameter, this.format.local, "", this.format.override);
}
}
}
}
}
private setResponseHeaderNames(response: Response) {
if (response.protocol.http) {
for (const { value: header } of items(response.protocol.http.headers)) {
setName(header as { language: Languages }, this.format.responseHeader, "", this.format.override);
}
}
}
fixParameterCollisions() {
for (const operation of values(this.codeModel.operationGroups).selectMany((each) => each.operations)) {
for (const request of values(operation.requests)) {
const parameters = values(operation.signatureParameters).concat(values(request.signatureParameters));
const usedNames = new Set<string>();
const collisions = new Set<Parameter>();
// we need to make sure we avoid name collisions. operation parameters get first crack.
for (const each of values(parameters)) {
const name = this.format.parameter(each.language.default.name);
if (usedNames.has(name)) {
collisions.add(each);
} else {
usedNames.add(name);
}
}
// handle operation parameters
for (const parameter of collisions) {
let options = [parameter.language.default.name];
if (isVirtualParameter(parameter)) {
options = getNameOptions(parameter.schema.language.default.name, [
parameter.language.default.name,
...parameter.pathToProperty.map((each) => each.language.default.name),
]).map((each) => this.format.parameter(each));
}
parameter.language.default.name = this.format.parameter(selectName(options, usedNames));
}
}
}
}
fixCollisions(schema: ObjectSchema) {
for (const each of values(schema.parents?.immediate).where((each) => isObjectSchema(each))) {
this.fixCollisions(<ObjectSchema>each);
}
const [owned, flattened] = values(schema.properties).bifurcate((each) => length(each.flattenedNames) === 0);
const inherited = [...getAllParentProperties(schema)];
const all = [...owned, ...inherited, ...flattened];
const inlined = new Map<string, number>();
for (const each of all) {
const name = this.format.property(each.language.default.name);
// track number of instances of a given name.
inlined.set(name, (inlined.get(name) || 0) + 1);
}
const usedNames = new Set(inlined.keys());
for (const each of flattened /*.sort((a, b) => length(a.nameOptions) - length(b.nameOptions)) */) {
const ct = inlined.get(this.format.property(each.language.default.name));
if (ct && ct > 1) {
const options = getNameOptions(each.schema.language.default.name, [
each.language.default.name,
...values(each.flattenedNames),
]);
each.language.default.name = this.format.property(selectName(options, usedNames));
}
}
}
fixPropertyCollisions() {
for (const schema of values(this.codeModel.schemas.objects)) {
this.fixCollisions(schema);
}
}
}

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

@ -0,0 +1,538 @@
import { Session } from "@azure-tools/autorest-extension-base";
import { values, items, length, Dictionary, refCount, clone, keys } from "@azure-tools/linq";
import {
Model as oai3,
Refable,
Dereferenced,
dereference,
Schema,
PropertyDetails,
JsonType,
StringFormat,
} from "@azure-tools/openapi";
import { serialize } from "@azure-tools/codegen";
import { Host, startSession } from "@azure-tools/autorest-extension-base";
import { Interpretations } from "../modeler/interpretations";
import { getDiff } from "recursive-diff";
import { ModelerFourOptions } from "../modeler/modelerfour-options";
export async function processRequest(host: Host) {
const debug = (await host.GetValue("debug")) || false;
try {
const session = await startSession<oai3>(host);
// process
const plugin = await new QualityPreChecker(session).init();
const input = plugin.input;
// go!
const result = plugin.process();
// throw on errors.
if (!(await session.getValue("ignore-errors", false))) {
session.checkpoint();
}
host.WriteFile(
"prechecked-openapi-document.yaml",
serialize(result, { sortKeys: false }),
undefined,
"prechecked-openapi-document",
);
host.WriteFile(
"original-openapi-document.yaml",
serialize(input, { sortKeys: false }),
undefined,
"openapi-document",
);
} catch (E) {
if (debug) {
console.error(`${__filename} - FAILURE ${JSON.stringify(E)} ${E.stack}`);
}
throw E;
}
}
interface DereferencedSchema {
key: string;
value: Dereferenced<Schema>;
}
export class QualityPreChecker {
input: oai3;
options: ModelerFourOptions = {};
protected interpret: Interpretations;
constructor(protected session: Session<oai3>) {
this.input = session.model; // shadow(session.model, filename);
this.interpret = new Interpretations(session);
}
async init() {
// get our configuration for this run.
this.options = await this.session.getValue("modelerfour", {});
return this;
}
private resolve<T>(item: Refable<T>): Dereferenced<T> {
return dereference(this.input, item);
}
getProperties(schema: Schema) {
return items(schema.properties).select((each) => ({
key: each.key,
name: <string>this.interpret.getPreferredName(each.value, each.key),
property: this.resolve(each.value).instance,
}));
//return items(schema.properties).toMap(each => <string>this.interpret.getPreferredName(each.value, each.key), each => this.resolve(each.value).instance);
}
getSchemasFromArray(
tag: string,
schemas: Array<Refable<Schema>> | undefined,
): Iterable<{ name: string; schema: Schema; tag: string }> {
return values(schemas).select((a) => {
const { instance: schema, name } = this.resolve(a);
return {
name: this.interpret.getName(name, schema),
schema,
tag,
};
});
}
*getAllParents(tag: string, schema: Schema): Iterable<{ name: string; schema: Schema; tag: string }> {
for (const parent of this.getSchemasFromArray(tag, schema.allOf)) {
yield parent;
yield* this.getAllParents(parent.name, parent.schema);
}
}
*getGrandParents(tag: string, schema: Schema): Iterable<{ name: string; schema: Schema; tag: string }> {
for (const parent of this.getSchemasFromArray(tag, schema.allOf)) {
yield* this.getAllParents(parent.name, parent.schema);
}
}
checkForHiddenProperties(schemaName: string, schema: Schema, completed = new WeakSet<Schema>()) {
if (completed.has(schema)) {
return;
}
completed.add(schema);
if (schema.allOf && schema.properties) {
const myProperties = this.getProperties(schema).toArray();
for (const { name: parentName, schema: parentSchema } of this.getAllParents(schemaName, schema)) {
this.checkForHiddenProperties(parentName, parentSchema, completed);
for (const { key, name: propName, property: parentProp } of this.getProperties(parentSchema)) {
const myProp = myProperties.find((each) => each.name === propName);
if (myProp) {
// check if the only thing different is the description.
const diff = getDiff(parentProp, myProp.property).filter(
(each) => each.path[0] !== "description" && each.path[0] !== "x-ms-metadata",
);
if (diff.length === 0) {
// the property didn't change except for description.
// we can let this go with a warning.
this.session.warning(
`Schema '${schemaName}' has a property '${propName}' that is already declared the parent schema '${parentName}' but isn't significantly different. The property has been removed from ${schemaName}`,
["PreCheck", "PropertyRedeclarationWarning"],
);
delete schema.properties[myProp.key];
continue;
}
if (diff.length === 1) {
// special case to yell about readonly changes
if (diff[0].path[0] === "readOnly") {
this.session.warning(
`Schema '${schemaName}' has a property '${propName}' that is already declared the parent schema '${parentName}' but 'readonly' has been changed -- this is not permitted. The property has been removed from ${schemaName}`,
["PreCheck", "PropertyRedeclarationWarning"],
);
delete schema.properties[myProp.key];
continue;
}
}
if (diff.length > 0) {
const details = diff
.map((each) => `${each.path.join(".")} => '${each.op === "delete" ? "<removed>" : each.val}'`)
.join(",");
this.session.error(
`Schema '${schemaName}' has a property '${propName}' that is conflicting with a property in the parent schema '${parentName}' differs more than just description : [${details}]`,
["PreCheck", "PropertyRedeclaration"],
);
continue;
}
}
}
}
}
}
checkForDuplicateParents(schemaName: string, schema: Schema, completed = new WeakSet<Schema>()) {
if (completed.has(schema)) {
return;
}
completed.add(schema);
if (schema.allOf) {
const grandParents = [...this.getGrandParents(schemaName, schema)];
const direct = [...this.getSchemasFromArray(schemaName, schema.allOf)];
for (const myParent of direct) {
for (const duplicate of grandParents.filter((each) => each.schema === myParent.schema)) {
this.session.error(
`Schema '${schemaName}' inherits '${duplicate.tag}' via an \`allOf\` that is already coming from parent '${myParent.name}'`,
["PreCheck", "DuplicateInheritance"],
);
}
}
}
}
isObjectOrEnum(schema: Schema) {
switch (schema.type) {
case JsonType.Array:
case JsonType.Boolean:
case JsonType.Number:
return false;
case JsonType.String:
return schema.enum || schema["x-ms-enum"];
case JsonType.Object:
// empty objects don't worry.
if (length(schema.properties) === 0 && length(schema.allOf) === 0) {
return false;
}
return true;
default:
return length(schema.properties) > 0 || length(schema.allOf) > 0 ? true : false;
}
}
checkForDuplicateSchemas(): undefined {
this.session.warning(
"Checking for duplicate schemas, this could take a (long) while. Run with --verbose for more detail.",
["PreCheck", "CheckDuplicateSchemas"],
);
// Returns true if scanning should be restarted
const innerCheckForDuplicateSchemas = (): any => {
const errors = new Set<string>();
if (this.input.components && this.input.components.schemas) {
const dupedNames = items(this.input.components?.schemas)
.select((s) => ({ key: s.key, value: this.resolve(s.value) }))
.groupBy(
// Make sure to check x-ms-client-name first to see if the schema is already being renamed
(each) => each.value.instance["x-ms-client-name"] || each.value.instance["x-ms-metadata"]?.name,
(each) => each,
);
for (const [name, schemas] of dupedNames.entries()) {
if (name && schemas.length > 1) {
const diff = getDiff(schemas[0].value.instance, schemas[1].value.instance).filter(
(each) => each.path[0] !== "description" && each.path[0] !== "x-ms-metadata",
);
if (diff.length === 0) {
// found two schemas that are indeed the same.
// stop, find all the $refs to the second one, and rewrite them to go to the first one.
// then go back and start again.
this.removeDuplicateSchemas(name, schemas[0], schemas[1]);
// Restart the scan now that the duplicate has been removed
return true;
}
// it may not be identical, but if it's not an object, I'm not sure we care too much.
if (values(schemas).any((each) => this.isObjectOrEnum(each.value.instance))) {
const rdiff = getDiff(schemas[1].value.instance, schemas[0].value.instance).filter(
(each) => each.path[0] !== "description" && each.path[0] !== "x-ms-metadata",
);
if (diff.length > 0) {
const details = diff
.map((each) => {
const path = each.path.join(".");
let iValue = each.op === "add" ? "<none>" : JSON.stringify(each.oldVal);
if (each.op !== "update") {
const v = rdiff.find((each) => each.path.join(".") === path);
iValue = JSON.stringify(v?.val);
}
const nValue = each.op === "delete" ? "<none>" : JSON.stringify(each.val);
return `${path}: ${iValue} => ${nValue}`;
})
.join(",");
errors.add(`Duplicate Schema named ${name} -- ${details} `);
continue;
}
}
}
}
}
for (const each of errors) {
// Allow duplicate schemas if requested
if (this.options["lenient-model-deduplication"]) {
this.session.warning(each, ["PreCheck", "DuplicateSchema"]);
} else {
this.session.error(
`${each}; This error can be *temporarily* avoided by using the 'modelerfour.lenient-model-deduplication' setting. NOTE: This setting will be removed in a future version of @autorest/modelerfour; schemas should be updated to fix this issue sooner than that.`,
["PreCheck", "DuplicateSchema"],
);
}
}
};
while (innerCheckForDuplicateSchemas()) {
// Loop until the scan is complete
}
return undefined;
}
private findSchemaToRemove(
schema1: DereferencedSchema,
schema2: DereferencedSchema,
): { keep: DereferencedSchema; remove: DereferencedSchema } {
const schema1Ref = this.input.components?.schemas?.[schema1.key].$ref;
// If schema1 is pointing to schema2 then we should delete schema1
if (schema1Ref && schema1Ref === `#/components/schemas/${schema2.key}`) {
return { remove: schema1, keep: schema2 };
}
return { remove: schema2, keep: schema1 };
}
private removeDuplicateSchemas(name: string, schema1: DereferencedSchema, schema2: DereferencedSchema) {
const { keep: schemaToKeep, remove: schemaToRemove } = this.findSchemaToRemove(schema1, schema2);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
delete this.input.components!.schemas![schemaToRemove.key];
const text = JSON.stringify(this.input);
this.input = JSON.parse(
text.replace(
new RegExp(`"\\#\\/components\\/schemas\\/${schemaToRemove.key}"`, "g"),
`"#/components/schemas/${schemaToKeep.key}"`,
),
);
// update metadata to match
if (this.input?.components?.schemas?.[schemaToKeep.key]) {
const primarySchema = this.resolve(this.input.components.schemas[schemaToKeep.key]);
const primaryMetadata = primarySchema.instance["x-ms-metadata"];
const secondaryMetadata = schemaToRemove.value.instance["x-ms-metadata"];
if (primaryMetadata && secondaryMetadata) {
primaryMetadata.apiVersions = [
...new Set<string>([...(primaryMetadata.apiVersions || []), ...(secondaryMetadata.apiVersions || [])]),
];
primaryMetadata.filename = [
...new Set<string>([...(primaryMetadata.filename || []), ...(secondaryMetadata.filename || [])]),
];
primaryMetadata.originalLocations = [
...new Set<string>([
...(primaryMetadata.originalLocations || []),
...(secondaryMetadata.originalLocations || []),
]),
];
primaryMetadata["x-ms-secondary-file"] = !(
!primaryMetadata["x-ms-secondary-file"] || !secondaryMetadata["x-ms-secondary-file"]
);
}
}
this.session.verbose(
`Schema ${name} has multiple identical declarations, reducing to just one - removing: ${schemaToRemove.key}, keeping: ${schemaToKeep.key}`,
["PreCheck", "ReducingSchema"],
);
}
fixUpSchemasThatUseAllOfInsteadOfJustRef() {
const schemas = this.input.components?.schemas;
if (schemas) {
for (const { key, instance: schema, name, fromRef } of items(schemas).select((s) => ({
key: s.key,
...this.resolve(s.value),
}))) {
// we're looking for schemas that offer no possible value
// because they just use allOf instead of $ref
if (!schema.type || schema.type === JsonType.Object) {
if (length(schema.allOf) === 1) {
if (length(schema.properties) > 0) {
continue;
}
if (schema.additionalProperties) {
continue;
}
const $ref = schema?.allOf?.[0]?.$ref;
const text = JSON.stringify(this.input);
this.input = JSON.parse(
text.replace(new RegExp(`"\\#\\/components\\/schemas\\/${key}"`, "g"), `"${$ref}"`),
);
const location = schema["x-ms-metadata"].originalLocations?.[0]?.replace(/^.*\//, "");
delete this.input.components?.schemas?.[key];
if (schema["x-internal-autorest-anonymous-schema"]) {
this.session.warning(
`An anonymous inline schema for property '${location.replace(
/-/g,
".",
)}' is using an 'allOf' instead of a $ref. This creates a wasteful anonymous type when generating code. Don't do that. - removing.`,
["PreCheck", "AllOfWhenYouMeantRef"],
);
} else {
this.session.warning(
`Schema '${location}' is using an 'allOf' instead of a $ref. This creates a wasteful anonymous type when generating code.`,
["PreCheck", "AllOfWhenYouMeantRef"],
);
}
}
}
}
}
}
fixUpObjectsWithoutType() {
for (const { instance: schema, name, fromRef } of values(this.input.components?.schemas).select((s) =>
this.resolve(s),
)) {
if (<any>schema.type === "file" || <any>schema.format === "file" || <any>schema.format === "binary") {
// handle inconsistency in file format handling.
this.session.hint(
`'The schema ${schema?.["x-ms-metadata"]?.name || name} with 'type: ${schema.type}', format: ${
schema.format
}' will be treated as a binary blob for binary media types.`,
["PreCheck", "BinarySchema"],
schema,
);
schema.type = JsonType.String;
schema.format = StringFormat.Binary;
}
switch (schema.type) {
case undefined:
case null:
if (schema.properties) {
// if the model has properties, then we're going to assume they meant to say JsonType.object
// but we're going to warn them anyway.
this.session.warning(
`The schema '${
schema?.["x-ms-metadata"]?.name || name
}' with an undefined type and decalared properties is a bit ambigious. This has been auto-corrected to 'type:object'`,
["PreCheck", "SchemaMissingType"],
schema,
);
schema.type = JsonType.Object;
break;
}
if (schema.additionalProperties) {
// this looks like it's going to be a dictionary
// we'll mark it as object and let the processObjectSchema sort it out.
this.session.warning(
`The schema '${
schema?.["x-ms-metadata"]?.name || name
}' with an undefined type and additionalProperties is a bit ambigious. This has been auto-corrected to 'type:object'`,
["PreCheck", "SchemaMissingType"],
schema,
);
schema.type = JsonType.Object;
break;
}
if (schema.allOf || schema.anyOf || schema.oneOf) {
// if the model has properties, then we're going to assume they meant to say JsonType.object
// but we're going to warn them anyway.
this.session.warning(
`The schema '${
schema?.["x-ms-metadata"]?.name || name
}' with an undefined type and 'allOf'/'anyOf'/'oneOf' is a bit ambigious. This has been auto-corrected to 'type:object'`,
["PreCheck", "SchemaMissingType"],
schema,
);
schema.type = JsonType.Object;
break;
}
break;
}
}
}
isEmptyObjectSchema(schema: Schema): boolean {
if (length(schema.properties) > 0 || length(schema.allOf) > 0 || schema.additionalProperties === true) {
return false;
}
if (schema.additionalProperties !== false) {
const resolved = this.resolve(schema.additionalProperties);
return !resolved.instance || this.isEmptyObjectSchema(resolved.instance);
}
return true;
}
fixUpSchemasWithEmptyObjectParent() {
const schemas = this.input.components?.schemas;
if (schemas) {
for (const { key, instance: schema, name, fromRef } of items(schemas).select((s) => ({
key: s.key,
...this.resolve(s.value),
}))) {
if (schema.type === JsonType.Object) {
if (length(schema.allOf) > 1) {
const schemaName = schema["x-ms-metadata"]?.name || name;
schema.allOf = schema.allOf?.filter((p) => {
const parent = this.resolve(p).instance;
if (this.isEmptyObjectSchema(parent)) {
this.session.warning(
`Schema '${schemaName}' has an allOf list with an empty object schema as a parent, removing it.`,
["PreCheck", "EmptyParentSchemaWarning"],
);
return false;
}
return true;
});
}
}
}
}
}
process() {
if (this.options["remove-empty-child-schemas"]) {
this.fixUpSchemasThatUseAllOfInsteadOfJustRef();
}
this.fixUpObjectsWithoutType();
this.fixUpSchemasWithEmptyObjectParent();
this.checkForDuplicateSchemas();
let onlyOnce = new WeakSet<Schema>();
for (const { instance: schema, name, fromRef } of values(this.input.components?.schemas).select((s) =>
this.resolve(s),
)) {
this.checkForHiddenProperties(this.interpret.getName(name, schema), schema, onlyOnce);
}
onlyOnce = new WeakSet<Schema>();
for (const { instance: schema, name, fromRef } of values(this.input.components?.schemas).select((s) =>
this.resolve(s),
)) {
this.checkForDuplicateParents(this.interpret.getName(name, schema), schema, onlyOnce);
}
return this.input;
}
}

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

@ -0,0 +1,108 @@
import { MatcherState } from "expect";
import * as fs from "fs";
import SnapshotState from "jest-snapshot/build/State";
import * as path from "path";
declare global {
namespace jest {
interface Matchers<R> {
toMatchRawFileSnapshot(snapshotFile: string): CustomMatcherResult;
}
}
}
function getAbsolutePathToSnapshot(testPath: string, snapshotFile: string) {
return path.isAbsolute(snapshotFile) ? snapshotFile : path.resolve(path.dirname(testPath), snapshotFile);
}
type Context = jest.MatcherUtils &
MatcherState & {
snapshotState: SnapshotState;
};
/**
* Helper
*/
function toMatchRawFileSnapshot(
this: Context,
received: object | Array<object>,
filename: string,
): jest.CustomMatcherResult {
if (typeof received !== "string") {
throw new Error("toMatchRawFileSnapshot is only supported with raw text");
}
if (this.isNot) {
return {
pass: true, // Will get inverted because of the .not
message: () => `.${this.utils.BOLD_WEIGHT("not")} cannot be used with snapshot matchers`,
};
}
const filepath = getAbsolutePathToSnapshot(this.testPath!, filename);
const content: string = received;
const updateSnapshot: "none" | "all" | "new" = (this.snapshotState as any)._updateSnapshot;
const coloredFilename = this.utils.DIM_COLOR(filename);
const errorColor = this.utils.RECEIVED_COLOR;
if (updateSnapshot === "none" && !fs.existsSync(filepath)) {
// We're probably running in CI environment
this.snapshotState.unmatched++;
return {
pass: false,
message: () =>
`New output file ${coloredFilename} was ${errorColor("not written")}.\n\n` +
"The update flag must be explicitly passed to write a new snapshot.\n\n",
};
}
if (fs.existsSync(filepath)) {
const output = fs.readFileSync(filepath, "utf8").replace(/\r\n/g, "\n");
// The matcher is being used with `.not`
if (output === content) {
this.snapshotState.matched++;
return { pass: true, message: () => "" };
} else {
if (updateSnapshot === "all") {
fs.mkdirSync(path.dirname(filepath), { recursive: true });
fs.writeFileSync(filepath, content);
this.snapshotState.updated++;
return { pass: true, message: () => "" };
} else {
this.snapshotState.unmatched++;
return {
pass: false,
message: () =>
`Received content ${errorColor("doesn't match")} the file ${coloredFilename}.\n\n${this.utils.diff(
content,
output,
)}`,
};
}
}
} else {
if (updateSnapshot === "new" || updateSnapshot === "all") {
fs.mkdirSync(path.dirname(filepath), { recursive: true });
fs.writeFileSync(filepath, content);
this.snapshotState.added++;
return { pass: true, message: () => "" };
} else {
this.snapshotState.unmatched++;
return {
pass: true,
message: () => `The output file ${coloredFilename} ${errorColor("doesn't exist")}.`,
};
}
}
}
expect.extend({ toMatchRawFileSnapshot } as any);

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

@ -0,0 +1,37 @@
import { CodeModel } from "@azure-tools/codemodel";
import { Model } from "@azure-tools/openapi";
import { ModelerFour } from "../../src/modeler/modelerfour";
import { ModelerFourOptions } from "modeler/modelerfour-options";
import { createTestSessionFromModel } from "../utils";
const modelerfourOptions: ModelerFourOptions = {
"flatten-models": true,
"flatten-payloads": true,
"group-parameters": true,
"resolve-schema-name-collisons": true,
"additional-checks": true,
"always-create-accept-parameter": true,
//'always-create-content-type-parameter': true,
"naming": {
override: {
$host: "$host",
cmyk: "CMYK",
},
local: "_ + camel",
constantParameter: "pascal",
},
};
const cfg = {
"modelerfour": modelerfourOptions,
"payload-flattening-threshold": 2,
};
export async function runModeler(spec: any, config: { modelerfour: ModelerFourOptions } = cfg): Promise<CodeModel> {
const { session, errors } = await createTestSessionFromModel<Model>(config, spec);
const modeler = await new ModelerFour(session).init();
expect(errors.length).toBe(0);
return modeler.process();
}

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

@ -0,0 +1,52 @@
import { addOperation, createTestSpec } from "../utils";
import { runModeler } from "./modelerfour-utils";
describe("Modelerfour.Request", () => {
describe("Body", () => {
describe("Required attribute", () => {
const runModelerWithBody = async (body: any) => {
const spec = createTestSpec();
addOperation(spec, "/test", {
post: {
requestBody: {
...body,
},
},
});
const codeModel = await runModeler(spec);
const parameter = codeModel.operationGroups[0]?.operations[0]?.requests?.[0]?.parameters?.[0];
expect(parameter).not.toBeNull();
return parameter;
};
const defaultBody = {
content: {
"application/octet-stream": {
schema: {
type: "object",
format: "file",
},
},
},
};
it("mark body as required if required: true", async () => {
const parameter = await runModelerWithBody({ ...defaultBody, required: true });
expect(parameter?.required).toBe(true);
});
it("mark body as not required if required: false", async () => {
const parameter = await runModelerWithBody({ ...defaultBody, required: true });
expect(parameter?.required).toBe(true);
});
it("mark body as not required by default", async () => {
const parameter = await runModelerWithBody(defaultBody);
expect(parameter?.required).toBe(undefined);
});
});
});
});

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -0,0 +1,63 @@
import { CodeModel, ObjectSchema } from "@azure-tools/codemodel";
import { ModelerFourOptions } from "modeler/modelerfour-options";
import { PreNamer } from "../../src/prenamer/prenamer";
import { createTestSessionFromModel } from "../utils";
const runPrenamer = async (model: CodeModel, options: ModelerFourOptions = {}) => {
const { session } = await createTestSessionFromModel<CodeModel>({ modelerfour: options }, model);
const prenamer = new PreNamer(session);
await prenamer.init();
return prenamer.process();
};
describe("Prenamer", () => {
let model: CodeModel;
beforeEach(() => {
model = new CodeModel("TestPrenamer");
});
describe("Renaming objects", () => {
it("Remove duplicate consecutive words by default", async () => {
model.schemas.add(new ObjectSchema("FooBarBar", "Description"));
const result = await runPrenamer(model);
expect(result.schemas.objects?.[0].language.default.name).toEqual("FooBar");
});
it("Keeps duplicate consecutive words if the new name already exists", async () => {
model.schemas.add(new ObjectSchema("FooBar", "Description"));
model.schemas.add(new ObjectSchema("FooBarBar", "Description"));
const result = await runPrenamer(model);
expect(result.schemas.objects?.[0].language.default.name).toEqual("FooBar");
expect(result.schemas.objects?.[1].language.default.name).toEqual("FooBarBar");
});
it("Keeps duplicate consecutive words if the new name already exists and still style the word", async () => {
model.schemas.add(new ObjectSchema("FooBar", "Description"));
model.schemas.add(new ObjectSchema("fooBar-Bar", "Description"));
const result = await runPrenamer(model);
expect(result.schemas.objects?.[0].language.default.name).toEqual("FooBar");
expect(result.schemas.objects?.[1].language.default.name).toEqual("FooBarBar");
});
it("throws an error if all styling options are already taken", async () => {
model.schemas.add(new ObjectSchema("FooBar", "Description"));
model.schemas.add(new ObjectSchema("FooBarBar", "Description"));
model.schemas.add(new ObjectSchema("fooBar-Bar", "Description"));
await expect(() => runPrenamer(model)).rejects.toThrowError(
"Couldn't style name 'fooBar-Bar'. All of the following naming possibilities created duplicate names: [FooBar,FooBarBar]. You can try using 'modelerfour.lenient-model-deduplication' to allow such duplicates.",
);
});
it("creates duplicates if there is no options and lenient-model-deduplication is enabled", async () => {
model.schemas.add(new ObjectSchema("FooBar", "Description"));
model.schemas.add(new ObjectSchema("FooBarBar", "Description"));
const result = await runPrenamer(model, {
"lenient-model-deduplication": true,
});
expect(result.schemas.objects?.[0].language.default.name).toEqual("FooBar");
expect(result.schemas.objects?.[1].language.default.name).toEqual("FooBarAutoGenerated");
});
});
});

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

@ -0,0 +1,133 @@
import { addSchema, createTestSessionFromModel, createTestSpec } from "../utils";
import { QualityPreChecker } from "../../src/quality-precheck/prechecker";
import { Model, Refable, Dereferenced, dereference, Schema } from "@azure-tools/openapi";
import { ModelerFourOptions } from "modeler/modelerfour-options";
class PreCheckerClient {
private constructor(private input: Model, public result: Model) {}
resolve<T>(item: Refable<T>): Dereferenced<T> {
return dereference(this.input, item);
}
static async create(spec: Model, config: ModelerFourOptions = {}): Promise<PreCheckerClient> {
const { session, errors } = await createTestSessionFromModel<Model>({ modelerfour: config }, spec);
const prechecker = await new QualityPreChecker(session).init();
expect(errors.length).toBe(0);
return new PreCheckerClient(prechecker.input, prechecker.process());
}
}
describe("Prechecker", () => {
it("removes empty object schemas from allOf list when other parents are present", async () => {
const spec = createTestSpec();
addSchema(spec, "ParentSchema", {
type: "object",
nullable: true,
properties: {
hack: {
type: "boolean",
},
},
});
addSchema(spec, "ChildSchema", {
type: "object",
allOf: [{ type: "object" }, { $ref: "#/components/schemas/ParentSchema" }],
properties: {
childOfHack: {
type: "integer",
},
},
});
const client = await PreCheckerClient.create(spec);
const model = client.result;
const childSchemaRef = model.components?.schemas && model.components?.schemas["ChildSchema"];
if (childSchemaRef) {
const childSchema = client.resolve<Schema>(childSchemaRef);
expect(childSchema.instance.allOf?.length).toEqual(1);
const parent = client.resolve(childSchema.instance.allOf && childSchema.instance.allOf[0]);
expect(parent.name).toEqual("ParentSchema");
} else {
fail("No 'ChildSchema' found!");
}
});
it("remove the sibling schema with the $ref", async () => {
const spec = createTestSpec();
addSchema(spec, "SiblingSchema", {
$ref: "#/components/schemas/MainSchema",
});
addSchema(spec, "MainSchema", {
"type": "object",
"x-ms-client-name": "MainSchema",
"properties": {
name: {
type: "string",
},
},
});
const client = await PreCheckerClient.create(spec);
const model = client.result;
const schemas = model.components!.schemas!;
expect(schemas["SiblingSchema"]).toBeUndefined();
expect(schemas["MainSchema"]).not.toBeUndefined();
const mainSchema: Schema = schemas["MainSchema"] as any;
expect(mainSchema.properties?.name.type).toEqual("string");
});
describe("Remove child types with no additional properties", () => {
let spec: any;
beforeEach(() => {
spec = createTestSpec();
addSchema(spec, "ChildSchema", {
allOf: [{ $ref: "#/components/schemas/ParentSchema" }],
});
addSchema(spec, "ParentSchema", {
type: "object",
properties: {
name: {
type: "string",
},
},
});
addSchema(spec, "Foo", {
type: "object",
properties: {
child: { $ref: "#/components/schemas/ChildSchema" },
},
});
});
it("Doesn't touch it by default", async () => {
const client = await PreCheckerClient.create(spec);
const schemas = client.result.components!.schemas!;
expect(schemas["ChildSchema"]).not.toBeUndefined();
expect(schemas["ParentSchema"]).not.toBeUndefined();
const foo = schemas["Foo"] as any;
expect(foo).not.toBeUndefined();
expect(foo.properties.child.$ref).toEqual("#/components/schemas/ChildSchema");
});
it("Remove the child type and points reference to it to its parent", async () => {
const client = await PreCheckerClient.create(spec, {
"remove-empty-child-schemas": true,
});
const schemas = client.result.components!.schemas!;
expect(schemas["ChildSchema"]).toBeUndefined();
expect(schemas["ParentSchema"]).not.toBeUndefined();
const foo = schemas["Foo"] as any;
expect(foo).not.toBeUndefined();
expect(foo.properties.child.$ref).toEqual("#/components/schemas/ParentSchema");
});
});
});

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

@ -0,0 +1,107 @@
!<!CodeModel>
info: !<!Info>
description: Acceptance test for file with json content type.
title: 'Binary with content-type: application/json'
schemas: !<!Schemas>
strings:
- !<!StringSchema> &ref_0
type: string
language: !<!Languages>
default:
name: string
description: simple string
protocol: !<!Protocols> {}
binaries:
- !<!BinarySchema> &ref_1
type: binary
language: !<!Languages>
default:
name: binary
description: ''
protocol: !<!Protocols> {}
globalParameters:
- !<!Parameter> &ref_3
schema: *ref_0
clientDefaultValue: ''
implementation: Client
origin: 'modelerfour:synthesized/host'
required: true
extensions:
x-ms-skip-url-encoding: true
language: !<!Languages>
default:
name: $host
description: server parameter
serializedName: $host
protocol: !<!Protocols>
http: !<!HttpParameter>
in: uri
operationGroups:
- !<!OperationGroup>
$key: Upload
operations:
- !<!Operation>
apiVersions:
- !<!ApiVersion>
version: 1.0.0
parameters:
- *ref_3
requests:
- !<!Request>
parameters:
- !<!Parameter> &ref_2
schema: *ref_1
implementation: Method
required: true
language: !<!Languages>
default:
name: data
description: Foo bar
protocol: !<!Protocols>
http: !<!HttpParameter>
in: body
style: binary
signatureParameters:
- *ref_2
language: !<!Languages>
default:
name: ''
description: ''
protocol: !<!Protocols>
http: !<!HttpBinaryRequest>
path: /file
method: post
binary: true
knownMediaType: binary
mediaTypes:
- application/json
uri: '{$host}'
signatureParameters: []
responses:
- !<!Response>
language: !<!Languages>
default:
name: ''
description: Upload successful
protocol: !<!Protocols>
http: !<!HttpResponse>
statusCodes:
- '204'
language: !<!Languages>
default:
name: File
description: Uploading json file
protocol: !<!Protocols> {}
language: !<!Languages>
default:
name: Upload
description: ''
protocol: !<!Protocols> {}
security: !<!Security>
authenticationRequired: false
language: !<!Languages>
default:
name: ''
description: ''
protocol: !<!Protocols>
http: !<!HttpModel> {}

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

@ -0,0 +1,107 @@
!<!CodeModel>
info: !<!Info>
description: test
title: Test
schemas: !<!Schemas>
strings:
- !<!StringSchema> &ref_2
type: string
language: !<!Languages>
default:
name: string
description: simple string
protocol: !<!Protocols> {}
- !<!StringSchema> &ref_0
type: string
language: !<!Languages>
default:
name: ''
description: ''
protocol: !<!Protocols> {}
dictionaries:
- !<!DictionarySchema> &ref_1
type: dictionary
elementType: *ref_0
language: !<!Languages>
default:
name: ''
description: Dictionary of <>
protocol: !<!Protocols> {}
- !<!DictionarySchema> &ref_3
type: dictionary
elementType: *ref_1
language: !<!Languages>
default:
name: DictOfDict
description: Dictionary of <>
protocol: !<!Protocols> {}
globalParameters:
- !<!Parameter> &ref_4
schema: *ref_2
clientDefaultValue: ''
implementation: Client
origin: 'modelerfour:synthesized/host'
required: true
extensions:
x-ms-skip-url-encoding: true
language: !<!Languages>
default:
name: $host
description: server parameter
serializedName: $host
protocol: !<!Protocols>
http: !<!HttpParameter>
in: uri
operationGroups:
- !<!OperationGroup>
$key: dictionary
operations:
- !<!Operation>
apiVersions:
- !<!ApiVersion>
version: test-0.1
parameters:
- *ref_4
requests:
- !<!Request>
language: !<!Languages>
default:
name: ''
description: ''
protocol: !<!Protocols>
http: !<!HttpRequest>
path: /dictionary/nested
method: get
uri: '{$host}'
signatureParameters: []
responses:
- !<!SchemaResponse>
schema: *ref_3
language: !<!Languages>
default:
name: ''
protocol: !<!Protocols>
http: !<!HttpResponse>
knownMediaType: json
mediaTypes:
- application/json
statusCodes:
- '200'
language: !<!Languages>
default:
name: getDictionaryValid
description: ''
protocol: !<!Protocols> {}
language: !<!Languages>
default:
name: dictionary
description: ''
protocol: !<!Protocols> {}
security: !<!Security>
authenticationRequired: false
language: !<!Languages>
default:
name: ''
description: ''
protocol: !<!Protocols>
http: !<!HttpModel> {}

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

@ -0,0 +1,36 @@
{
"openapi": "3.0.0",
"info": {
"title": "Binary with content-type: application/json",
"description": "Acceptance test for file with json content type.",
"version": "1.0.0"
},
"paths": {
"/file": {
"x-ms-metadata": {
"apiVersions": ["1.0.0"]
},
"post": {
"description": "Uploading json file",
"operationId": "Upload_File",
"requestBody": {
"description": "Foo bar",
"content": {
"application/json": {
"schema": {
"type": "object",
"format": "file"
}
}
},
"required": true
},
"responses": {
"204": {
"description": "Upload successful"
}
}
}
}
}
}

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

@ -0,0 +1,47 @@
{
"openapi": "3.0.0",
"info": {
"x-ms-metadata": {
"apiVersions": ["test-0.1"]
},
"title": "Test",
"description": "test",
"version": "test-0.1"
},
"paths": {
"/dictionary/nested": {
"x-ms-metadata": {
"apiVersions": ["test-0.1"]
},
"get": {
"operationId": "dictionary_getDictionaryValid",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DictOfDict"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"DictOfDict": {
"type": "object",
"additionalProperties": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"servers": []
}

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

@ -0,0 +1,61 @@
import { createTestSessionFromFiles } from "../utils";
import { ModelerFour } from "../../src/modeler/modelerfour";
import { readdirSync } from "fs";
import { serialize } from "@azure-tools/codegen";
import { Model } from "@azure-tools/openapi";
import { codeModelSchema } from "@azure-tools/codemodel";
const cfg = {
"modelerfour": {
"flatten-models": true,
"flatten-payloads": true,
"group-parameters": true,
"resolve-schema-name-collisons": true,
"additional-checks": true,
//'always-create-content-type-parameter': true,
"naming": {
override: {
$host: "$host",
cmyk: "CMYK",
},
local: "_ + camel",
constantParameter: "pascal",
/*
for when playing with python style settings :
parameter: 'snakecase',
property: 'snakecase',
operation: 'snakecase',
operationGroup: 'pascalcase',
choice: 'pascalcase',
choiceValue: 'uppercase',
constant: 'uppercase',
type: 'pascalcase',
// */
},
},
"payload-flattening-threshold": 2,
};
const inputsFolder = `${__dirname}/inputs/`;
const expectedFolder = `${__dirname}/expected/`;
describe("Testing rendering specific scenarios", () => {
const folders = readdirSync(inputsFolder);
for (const folder of folders) {
it(`generate model for '${folder}'`, async () => {
const { session, errors } = await createTestSessionFromFiles<Model>(cfg, `${inputsFolder}/${folder}`, [
"openapi-document.json",
]);
expect(errors.length).toBe(0);
const modeler = await new ModelerFour(session).init();
const codeModel = modeler.process();
const yaml = serialize(codeModel, codeModelSchema);
expect(yaml).toMatchRawFileSnapshot(`${expectedFolder}/${folder}/modeler.yaml`);
});
}
});

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

@ -0,0 +1 @@
import "./custom-matchers";

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

@ -0,0 +1,3 @@
export * from "./test-session";
export * from "./specs";
export * from "./schema-utils";

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

@ -0,0 +1,23 @@
export function response(
code: number | "default",
contentType: string,
schema: any,
description = "The response.",
extraProperties?: any,
) {
return {
[code]: {
description,
content: {
[contentType]: {
schema,
},
},
...extraProperties,
},
};
}
export function responses(...responses: Array<any>) {
return responses.reduce((responsesDict, response) => Object.assign(responsesDict, response), {});
}

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

@ -0,0 +1,43 @@
import { clone } from "@azure-tools/linq";
export const InitialTestSpec = Object.freeze({
info: {
title: "Test OpenAPI 3 Specification",
description: "A test document",
contact: {
name: "Microsoft Corporation",
url: "https://microsoft.com",
email: "devnull@microsoft.com",
},
license: "MIT",
version: "1.0",
},
paths: {},
components: {
schemas: {},
},
});
export type TestSpecCustomizer = (spec: any) => any;
export function createTestSpec(...customizers: Array<TestSpecCustomizer>): any {
return customizers.reduce<any>(
(spec: any, customizer: TestSpecCustomizer) => customizer(spec),
clone(InitialTestSpec),
);
}
export function addOperation(
spec: any,
path: string,
operationDict: any,
metadata: any = { apiVersions: ["1.0.0"] },
): void {
operationDict = { ...operationDict, ...{ "x-ms-metadata": metadata } };
spec.paths[path] = operationDict;
}
export function addSchema(spec: any, name: string, schemaDict: any, metadata: any = { apiVersions: ["1.0.0"] }): void {
schemaDict = { ...schemaDict, ...{ "x-ms-metadata": metadata } };
spec.components.schemas[name] = schemaDict;
}

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

@ -0,0 +1,79 @@
import { readFile } from "@azure-tools/async-io";
import { deserialize, fail } from "@azure-tools/codegen";
import { Session, startSession } from "@azure-tools/autorest-extension-base";
import { Model } from "@azure-tools/openapi";
export interface TestSessionInput {
model: any;
filename: string;
content: string;
}
export interface TestSession<T> {
session: Session<T>;
errors: Array<any>;
}
async function readData(folder: string, ...files: Array<string>): Promise<Map<string, TestSessionInput>> {
const results = new Map<string, { model: any; filename: string; content: string }>();
for (const filename of files) {
const content = await readFile(`${folder}/${filename}`);
const model = deserialize<any>(content, filename);
results.set(filename, {
model,
filename,
content,
});
}
return results;
}
export async function createTestSessionFromFiles<TInputModel>(
config: any,
folder: string,
inputs: Array<string>,
): Promise<TestSession<TInputModel>> {
const models = await readData(folder, ...inputs);
return createTestSession(config, models);
}
export async function createTestSessionFromModel<TInputModel>(
config: any,
model: any,
): Promise<TestSession<TInputModel>> {
return createTestSession(config, [
{
model: model,
filename: "openapi-3.json",
content: JSON.stringify(model),
},
]);
}
export async function createTestSession<TInputModel>(
config: any,
inputs: Array<TestSessionInput> | Map<string, TestSessionInput>,
): Promise<TestSession<TInputModel>> {
const models = Array.isArray(inputs) ? inputs.reduce((m, x) => m.set(x.filename, x), new Map()) : inputs;
const errors: Array<any> = [];
const session = await startSession<TInputModel>({
ReadFile: (filename: string) =>
Promise.resolve(models.get(filename)?.content ?? fail(`missing input '${filename}'`)),
GetValue: (key: string) => Promise.resolve(key ? config[key] : config),
ListInputs: (artifactType?: string) => Promise.resolve([...models.values()].map((x) => x.filename)),
ProtectFiles: (path: string) => Promise.resolve(),
WriteFile: (filename: string, content: string, sourceMap?: any, artifactType?: string) => Promise.resolve(),
Message: (message: any): void => {
if (message.Channel === "warning" || message.Channel === "error" || message.Channel === "verbose") {
// console.error(`${message.Channel} ${message.Text}`);
if (message.Channel === "error") {
errors.push(message);
}
}
},
UpdateConfigurationFile: (filename: string, content: string) => {},
GetConfigurationFile: (filename: string) => Promise.resolve(""),
});
return { session, errors };
}

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

@ -0,0 +1,8 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"baseUrl": "src"
},
"include": ["src/**/*.ts", "test/**/*.ts"]
}

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

@ -43,7 +43,7 @@
"mocha-typescript": "1.1.17",
"@typescript-eslint/eslint-plugin": "^4.12.0",
"@typescript-eslint/parser": "^4.12.0",
"eslint-plugin-prettier": "~3.1.4",
"eslint-plugin-prettier": "~3.2.0",
"eslint": "^7.17.0",
"typescript": "~3.9.7",
"glob": "~7.1.4",
@ -53,7 +53,7 @@
},
"dependencies": {
"@azure-tools/autorest-extension-base": "~3.1.0",
"@azure-tools/codegen": "~2.5.0",
"@azure-tools/codegen": "2.5.290",
"@azure-tools/linq": "~3.1.0"
},
"publishConfig": {

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

@ -34,7 +34,7 @@
"@typescript-eslint/parser": "^4.12.0",
"eslint": "^7.17.0",
"eslint-plugin-import": "~2.22.1",
"eslint-plugin-prettier": "~3.1.4",
"eslint-plugin-prettier": "~3.2.0",
"eslint-plugin-unicorn": "~25.0.1",
"prettier": "~2.2.1",
"supertest": "^6.0.1",

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

@ -4,7 +4,9 @@
"pnpmVersion": "5.13.5",
"pnpmOptions": {},
"nodeSupportedVersionRange": ">=8.9.4 <15.0.0",
"ensureConsistentVersions": true,
// Disabling this during migration of other repos as it is causing issues.
// This needs to be enabled again. https://github.com/Azure/autorest/issues/3807
"ensureConsistentVersions": false,
"projectFolderMinDepth": 3,
"projectFolderMaxDepth": 3,
"repository": {},
@ -45,6 +47,12 @@
"projectFolder": "packages/libs/codemodel",
"reviewCategory": "production",
"shouldPublish": true
},
{
"packageName": "@autorest/modelerfour",
"projectFolder": "packages/extensions/modelerfour",
"reviewCategory": "production",
"shouldPublish": true
}
]
}