зеркало из https://github.com/Azure/autorest.git
Move @autorest/modelerfour package (#3806)
This commit is contained in:
Родитель
1da414a793
Коммит
3b80bf9506
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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",
|
||||
|
|
10
rush.json
10
rush.json
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче