diff --git a/Dockerfile b/Dockerfile index 543bc169f06d..2abb4c66345e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ COPY ./google_checkstyle.xml ${GEN_DIR} # All poms are copied, then we go offline, to allow for better caching of code changes without fetching all dependencies each time COPY ./modules/openapi-generator-gradle-plugin/pom.xml ${GEN_DIR}/modules/openapi-generator-gradle-plugin/ COPY ./modules/openapi-generator-maven-plugin/pom.xml ${GEN_DIR}/modules/openapi-generator-maven-plugin/ +COPY ./modules/openapi-generator-mill-plugin/pom.xml ${GEN_DIR}/modules/openapi-generator-mill-plugin/ COPY ./modules/openapi-generator-online/pom.xml ${GEN_DIR}/modules/openapi-generator-online/ COPY ./modules/openapi-generator-cli/pom.xml ${GEN_DIR}/modules/openapi-generator-cli/ COPY ./modules/openapi-generator-core/pom.xml ${GEN_DIR}/modules/openapi-generator-core/ @@ -23,6 +24,7 @@ RUN mvn dependency:go-offline # Modules are copied individually here to allow for caching of docker layers between major.minor versions COPY ./modules/openapi-generator-gradle-plugin ${GEN_DIR}/modules/openapi-generator-gradle-plugin COPY ./modules/openapi-generator-maven-plugin ${GEN_DIR}/modules/openapi-generator-maven-plugin +COPY ./modules/openapi-generator-mill-plugin ${GEN_DIR}/modules/openapi-generator-mill-plugin COPY ./modules/openapi-generator-online ${GEN_DIR}/modules/openapi-generator-online COPY ./modules/openapi-generator-cli ${GEN_DIR}/modules/openapi-generator-cli COPY ./modules/openapi-generator-core ${GEN_DIR}/modules/openapi-generator-core diff --git a/docs/plugins.md b/docs/plugins.md index 30d54a8da756..0679eb96088b 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -120,3 +120,56 @@ openApiGenerate { ``` *If you want to create separate tasks (for example when you have more than one api spec and require different parameters for each), this is how to do so in Gradle 7+: `tasks.register('taskName', org.openapitools.generator.gradle.plugin.tasks.GenerateTask) { ... }`.* + +## Mill + +This Mill library provides a Mill module that can be used to generate code from OpenAPI specifications. + +### Example + +```scala +//| mill-version: 1.0.6 +//| mvnDeps: +//| - org.openapitools:openapi-generator-mill-plugin:7.19.0 # 1. + +import mill.* + +import org.openapitools.generator.mill.OpenApiModule // 2. + +object `package` extends JavaModule with MavenModule with OpenApiModule { // 3. + + // other Mill config... + + object openapi extends OpenApiConfig { // 4. + def inputSpec: T[PathRef] = Task.Source(BuildCtx.workspaceRoot / "api" / "petstore.yaml") + // other config options... + } + + override def generatedSources: T[Seq[PathRef]] = Seq( + PathRef(Task.dest), + openapi.generate(), // 5. + ) +} +``` + +1. Add the plugin to your `build.mill` as `mvnDeps` in the header section +2. import `org.openapitools.generator.mill.OpenApiModule` +3. add `OpenApiModule` to the module definition +4. configure 1-n `OpenApiConfig` as sub-modules +5. run the generation as part of the `compile` task + +This gives access to the following tasks: + +| Task | Description | +|---------------------------|---------------------------------------------------------------------------------------------| +| .generate | Generate code via Open API Tools Generator for Open API 2.0 or 3.x specification documents. | +| .validateSpec | Validates the configured spec | + +and a command + +| Command | Description | +|---------------------|------------------------------------------------| +| validateOpenapiSpec | Takes the path to a spec file and validates it | + + +For full details of all options, see the [plugin README](https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator-mill-plugin). diff --git a/modules/openapi-generator-mill-plugin/README.md b/modules/openapi-generator-mill-plugin/README.md new file mode 100644 index 000000000000..071b5760f084 --- /dev/null +++ b/modules/openapi-generator-mill-plugin/README.md @@ -0,0 +1,173 @@ +openapi-generator-mill-plugin +============================ + +A [Mill](https://mill-build.org) library to support the OpenAPI generator project. + +Usage +============================ + +1. Add the plugin to your `build.mill` as `mvnDeps` in the header section +2. import `org.openapitools.generator.mill.OpenApiModule` +3. add `OpenApiModule` to the module definition +4. configure 1-n `OpenApiConfig` as sub-modules + +```scala +//| mill-version: 1.0.6 +//| mvnDeps: +//| - org.openapitools:openapi-generator-mill-plugin:7.19.0 # 1. + +import mill.* + +import org.openapitools.generator.mill.OpenApiModule // 2. + +object `package` extends JavaModule with MavenModule with OpenApiModule { // 3. + + override def mvnDeps = Seq( + mvn"jakarta.platform:jakarta.jakartaee-api:11.0.0", + mvn"com.fasterxml.jackson.core:jackson-databind:2.20.0", + ) + + object openapi extends OpenApiConfig { // 4. + def inputSpec: T[PathRef] = Task.Source(BuildCtx.workspaceRoot / "api" / "petstore.yaml") + def apiPackage: T[String] = "com.acme.foo.boundary.web.api" + def modelPackage: T[String] = "com.acme.foo.boundary.web.model" + def generatorName: T[String] = "jaxrs-spec" + def sourceFolder: T[String] = "src/main/java" + + def additionalProperties: T[Map[String, String]] = Map( + "dateLibrary" -> "java8", + "useJakartaEe" -> "true", + "useSwaggerAnnotations" -> "false", + "interfaceOnly" -> "true", + "useTags" -> "true", + ) + } +} +``` + +Followed by: + +```bash +mill openapi.generate +``` + +Usually you want to include the generation to the `compile` phase and have the sources in your source-tree which can +be achieved by adding the generation task to the `generatedSources`. + +```scala +override def generatedSources: T[Seq[PathRef]] = Seq( + PathRef(Task.dest), + openapi.generate(), +) +``` + +Followed by: + +```bash +mill __.compile +``` + +This works because `generatedSources` expects a list of `PathRef`s which constitute all folders that contain additional +(generated) sources and the `generate` task from each `OpenApiConfig` returns a `PathRef` to the folder where it put the sources. + + +### General Configuration parameters for OpenApiConfig + +| Option | Description | +|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `verbose` | verbose mode (`false` by default) | +| `inputSpec` | OpenAPI Spec file path | +| `inputSpecRootDirectory` | Local root folder with spec file(s) | +| `mergedFileName` | Name of the file that will contain all merged specs | +| `generatorName` | target generator name | +| `cleanupOutput` | Defines whether the output directory should be cleaned up before generating the output (`false` by default). | +| `cleanup` | Defines a task which contains an `Option[Path => Unit]` which is called after the generation completed. Useful for instance to delete generated Types which are already replaced by import/type-mappings. | +| `gitSettings` | sets Git information of the project (with `host`, `userId` and `repoId`) | +| `templateDirectory` | directory with mustache templates | +| `engine` | The name of templating engine to use, "mustache" (default) or "handlebars" (beta) | +| `auth` | adds authorization headers when fetching the OpenAPI definitions remotely. Pass in a URL-encoded string of `name:header` with a comma separating multiple values | +| `skipOverwrite` | Specifies if the existing files should be overwritten during the generation. (`false` by default) | +| `apiPackage` | the package to use for generated api objects/classes | +| `modelPackage` | the package to use for generated model objects/classes | +| `invokerPackage` | the package to use for the generated invoker objects | +| `packageName` | the default package name to use for the generated objects | +| `artifactSettings` | sets project information in generated pom.xml/build.gradle or other build script. Language-specific conversions occur in non-jvm generators | +| `library` | library template (sub-template) | +| `modelNamePrefix` | Sets the prefix for model classes and enums | +| `modelNameSuffix` | Sets the suffix for model classes and enums | +| `apiNameSuffix` | Sets the suffix for api classes | +| `ignoreFileOverride` | specifies the full path to a `.openapi-generator-ignore` used for pattern based overrides of generated outputs | +| `httpUserAgent` | Sets custom User-Agent header value | +| `removeOperationIdPrefix` | remove operationId prefix (e.g. user_getName => getName) | +| `skipOperationExample` | skip examples defined in the operation | +| `logToStderr` | write all log messages (not just errors) to STDERR | +| `enablePostProcessFile` | post-processing hook | +| `skipValidateSpec` | Whether or not to skip validating the input spec prior to generation. By default, invalid specifications will result in an error. | +| `strictSpec` | Whether or not to treat an input document strictly against the spec. 'MUST' and 'SHALL' wording in OpenAPI spec is strictly adhered to. e.g. when false, no fixes will be applied to documents which pass validation but don't follow the spec. | +| `openapiNormalizer` | specifies the rules to be enabled in OpenAPI normalizer in the form of RULE_1=true,RULE_2=original. | +| `generateAliasAsModel` | generate alias (array, map) as model | +| `configOptions` | N/A | a **map** of generator-specific parameters. To show a full list of generator-specified parameters (options), please use `configHelp` (explained below) +| `importMappings` | specifies mappings between a given class and the import that should be used for that class in the format of type=import,type=import. You can also have multiple occurrences of this option | +| `typeMappings` | sets mappings between OpenAPI spec types and generated code types in the format of OpenAPIType=generatedType,OpenAPIType=generatedType. For example: `array=List,map=Map,string=String`. You can also have multiple occurrences of this option. To map a specified format, use type+format, e.g. string+password=EncryptedString will map `type: string, format: password` to `EncryptedString`. | +| `schemaMappings` | specifies mappings between the schema and the new name in the format of schema_a=Cat,schema_b=Bird. https://openapi-generator.tech/docs/customization/#schema-mapping | +| `nameMappings` | specifies mappings between the property name and the new name in the format of property_a=firstProperty,property_b=secondProperty. https://openapi-generator.tech/docs/customization/#name-mapping | +| `modelNameMappings` | specifies mappings between the model name and the new name in the format of model_a=FirstModel,model_b=SecondModel. https://openapi-generator.tech/docs/customization/#name-mapping | +| `parameterNameMappings` | specifies mappings between the parameter name and the new name in the format of param_a=first_parameter,param_b=second_parameter. https://openapi-generator.tech/docs/customization/#name-mapping | +| `inlineSchemaNameMappings` | specifies mappings between the inline schema name and the new name in the format of inline_object_2=Cat,inline_object_5=Bird. | +| `inlineSchemaOptions` | specifies the options used when naming inline schema in inline model resolver | +| `languageSpecificPrimitives` | specifies additional language specific primitive types in the format of type1,type2,type3,type3. For example: `String,boolean,Boolean,Double`. You can also have multiple occurrences of this option | +| `additionalProperties` | sets additional properties that can be referenced by the mustache templates in the format of name=value,name=value. You can also have multiple occurrences of this option | +| `reservedWordsMappings` | specifies how a reserved name should be escaped to. Otherwise, the default `_` is used. For example `id=identifier`. You can also have multiple occurrences of this option | +| `generateApis` | generate the apis (`true` by default). To generate only a subset, define via `apiFilesConstrainedTo`. | +| `apiFilesConstrainedTo` | A comma separated list of apis to generate. All apis is the default. | +| `generateModels` | generate the models (`true` by default). To generate only a subset, define via `modelFilesConstrainedTo`. | +| `modelFilesConstrainedTo` | A comma separated list of models to generate. All models is the default. | +| `generateRecursiveDependentModels` | Enables dependent Models to be generated when `modelFilesConstrainedTo` is used. Default depends on `modelFilesConstrainedTo` (true when nonEmpty) | +| `generateSupportingFiles` | generate the supporting files (`true` by default). To generate only a subset, define via `supportingFilesConstrainedTo`. | +| `supportingFilesConstrainedTo` | A list of supporting files to generate. When not defined, all files will be generated. | +| `generateModelTests` | generate the model tests (currently disabled) | +| `generateModelDocumentation` | generate the model documentation (`true` by default) | +| `generateApiTests` | generate the api tests (currently disabled) | +| `generateApiDocumentation` | generate the api documentation (`true` by default) | +| `dryRun` | Defines whether the generator should run in dry-run mode. In dry-run mode no files are written and a summary about file states is output ( `false` by default). | + +### Type and import mappings + +To override the mappings between OpenAPI spec types and the types used in the generated code, set `typeMappings`. + +```scala +def typeMappings: T[Map[String, String]] = Map( + "time" -> "LocalTime" +) +``` + +For types that are not already included in the generator configuration, you may need to add a corresponding `importMapping` too. + +```scala +def typeMappings: T[Map[String, String]] = Map( + "binary" -> "StreamingOutput", + "file" -> "StreamingOutput" +) +def importMappings: T[Map[String, String]] = Map( + "StreamingOutput" -> "javax.ws.rs.core.StreamingOutput", +) +``` + +### Validate Command + +You can validate any OpenAPI spec file by calling `validateOpenapiSpec` on the `OpenApiModule`. + +```bash +mill validateOpenapiSpec $(pwd)/api/petstore-v3.0-invalid.yaml +``` + +This command has two additional parameters: +* `--failOnWarnings true` enable failing the check already on warnings +* `--recommend false` + +You can also validate your `OpenApiConfig` object by calling `validate` on it. + +```bash +mill yourConfigObject.validate +``` + diff --git a/modules/openapi-generator-mill-plugin/mill-test/README.md b/modules/openapi-generator-mill-plugin/mill-test/README.md new file mode 100644 index 000000000000..a8040d15ce78 --- /dev/null +++ b/modules/openapi-generator-mill-plugin/mill-test/README.md @@ -0,0 +1,9 @@ +To test the Mill plugin, this project can be used. + +> ![NOTE] +> The `mill-build` folder is only needed to look up the plugin from the local Maven repository for development purposes. + +It requires that the current SNAPSHOT version of the plugin is installed in the local Maven repository. +Replace the version in `build.mill` or set the environment variable `$MILL_OPENAPITOOLS_PLUGIN_VERSION` to the desired version. + +Run `./mill __.compile` to test if the plugin works or some of the modules tasks like `./mill openapi.validate`. \ No newline at end of file diff --git a/modules/openapi-generator-mill-plugin/mill-test/api/petstore-v3.0-invalid.yaml b/modules/openapi-generator-mill-plugin/mill-test/api/petstore-v3.0-invalid.yaml new file mode 100644 index 000000000000..0f5c6fc29829 --- /dev/null +++ b/modules/openapi-generator-mill-plugin/mill-test/api/petstore-v3.0-invalid.yaml @@ -0,0 +1,103 @@ +openapi: "3.0.0" +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/modules/openapi-generator-mill-plugin/mill-test/api/petstore.yaml b/modules/openapi-generator-mill-plugin/mill-test/api/petstore.yaml new file mode 100644 index 000000000000..0d046ba209b0 --- /dev/null +++ b/modules/openapi-generator-mill-plugin/mill-test/api/petstore.yaml @@ -0,0 +1,736 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: OpenAPI Petstore + license: + name: Apache-2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + style: form + explode: false + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'read:pets' + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: >- + Multiple tags can be provided with comma separated strings. Use tag1, + tag2, tag3 for testing. + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + style: form + explode: false + schema: + type: array + items: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid tag value + security: + - petstore_auth: + - 'read:pets' + deprecated: true + '/pet/{petId}': + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - api_key: [] + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: '' + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid pet value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + '/pet/{petId}/uploadImage': + post: + tags: + - pet + summary: uploads an image + description: '' + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + additionalMetadata: + description: Additional data to pass to server + type: string + file: + description: file to upload + type: string + format: binary + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: '' + operationId: placeOrder + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid Order + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + description: order placed for purchasing the pet + required: true + '/store/order/{orderId}': + get: + tags: + - store + summary: Find purchase order by ID + description: >- + For valid response try integer IDs with value <= 5 or > 10. Other values + will generate exceptions + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of pet that needs to be fetched + required: true + schema: + type: integer + format: int64 + minimum: 1 + maximum: 5 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid ID supplied + '404': + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: >- + For valid response try integer IDs with value < 1000. Anything above + 1000 or nonintegers will generate API errors + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid ID supplied + '404': + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + responses: + default: + description: successful operation + security: + - api_key: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Created user object + required: true + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithArrayInput + responses: + default: + description: successful operation + security: + - api_key: [] + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithListInput + responses: + default: + description: successful operation + security: + - api_key: [] + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$' + - name: password + in: query + description: The password for login in clear text + required: true + schema: + type: string + responses: + '200': + description: successful operation + headers: + Set-Cookie: + description: >- + Cookie authentication key for use with the `api_key` + apiKey authentication. + schema: + type: string + example: AUTH_KEY=abcde12345; Path=/; HttpOnly + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: '' + operationId: logoutUser + responses: + default: + description: successful operation + security: + - api_key: [] + '/user/{username}': + get: + tags: + - user + summary: Get user by user name + description: '' + operationId: getUserByName + parameters: + - name: username + in: path + description: The name that needs to be fetched. Use user1 for testing. + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/User' + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid username supplied + '404': + description: User not found + put: + tags: + - user + summary: Updated user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid user supplied + '404': + description: User not found + security: + - api_key: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Updated user object + required: true + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid username supplied + '404': + description: User not found + security: + - api_key: [] +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + description: List of user object + required: true + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/api/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Order: + title: Pet Order + description: An order for a pets from the pet store + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order + Category: + title: Pet category + description: A category for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$' + xml: + name: Category + User: + title: a User + description: A User who is purchasing from the pet store + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + xml: + name: User + Tag: + title: Pet Tag + description: A tag for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + title: a Pet + description: A pet for sale in the pet store + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet + ApiResponse: + title: An uploaded response + description: Describes the result of uploading an image resource + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string diff --git a/modules/openapi-generator-mill-plugin/mill-test/build.mill b/modules/openapi-generator-mill-plugin/mill-test/build.mill new file mode 100644 index 000000000000..85a6ef8b981f --- /dev/null +++ b/modules/openapi-generator-mill-plugin/mill-test/build.mill @@ -0,0 +1,53 @@ +//| mill-version: 1.0.6 +//| mvnDeps: +//| - com.lihaoyi::mill-contrib-versionfile:$MILL_VERSION +//| - org.openapitools:openapi-generator-mill-plugin:$MILL_OPENAPITOOLS_PLUGIN_VERSION +//| +package build + +import mill.* +import mill.api.BuildCtx +import mill.javalib.publish.{Developer, License, VersionControl} +import mill.scalalib.publish.PomSettings +import mill.util.BuildInfo.{millBinPlatform, millVersion} +import javalib.* + +import mill.contrib.versionfile.VersionFileModule +import org.openapitools.generator.mill.OpenApiModule + +object `package` extends JavaModule with MavenModule with OpenApiModule { + + // dependency on openapi-generator is set in Mills meta-build + // this is needed to point to a local maven repo + override def mvnDeps = Seq( + mvn"jakarta.platform:jakarta.jakartaee-api:11.0.0", + mvn"com.fasterxml.jackson.core:jackson-databind:2.20.0", + mvn"org.openapitools:jackson-databind-nullable:0.2.8" + ) + + object openapi extends OpenApiConfig { + override def inputSpec: T[PathRef] = Task.Source(BuildCtx.workspaceRoot / "api" / "petstore.yaml") + override def apiPackage: T[String] = "com.acme.foo.boundary.web.api" + override def modelPackage: T[String] = "com.acme.foo.boundary.web.model" + override def generatorName: T[String] = "jaxrs-spec" + override def sourceFolder: T[String] = "src/gen/java" + + override def gitSettings: T[Option[GitSettings]] = Some(GitSettings("host", "userid", "repoid")) + override def artifactSettings: T[Option[ArtifactSettings]] = Some(ArtifactSettings("groupid", "artifactid", None)) + + override def cleanupOutput: T[Boolean] = true + + def additionalProperties: T[Map[String, String]] = Map( + "dateLibrary" -> "java8", + "useJakartaEe" -> "true", + "useSwaggerAnnotations" -> "false", + "interfaceOnly" -> "true", + "useTags" -> "true", + ) + } + + override def generatedSources: T[Seq[PathRef]] = Seq( + PathRef(Task.dest), + openapi.generate(), + ) +} \ No newline at end of file diff --git a/modules/openapi-generator-mill-plugin/mill-test/mill b/modules/openapi-generator-mill-plugin/mill-test/mill new file mode 100755 index 000000000000..4a0cb640bd7c --- /dev/null +++ b/modules/openapi-generator-mill-plugin/mill-test/mill @@ -0,0 +1,333 @@ +#!/usr/bin/env sh + +# This is a wrapper script, that automatically selects or downloads Mill from Maven Central or GitHub release pages. +# +# This script determines the Mill version to use by trying these sources +# - env-variable `MILL_VERSION` +# - local file `.mill-version` +# - local file `.config/mill-version` +# - `mill-version` from YAML fronmatter of current buildfile +# - if accessible, find the latest stable version available on Maven Central (https://repo1.maven.org/maven2) +# - env-variable `DEFAULT_MILL_VERSION` +# +# If a version has the suffix '-native' a native binary will be used. +# If a version has the suffix '-jvm' an executable jar file will be used, requiring an already installed Java runtime. +# If no such suffix is found, the script will pick a default based on version and platform. +# +# Once a version was determined, it tries to use either +# - a system-installed mill, if found and it's version matches +# - an already downloaded version under ~/.cache/mill/download +# +# If no working mill version was found on the system, +# this script downloads a binary file from Maven Central or Github Pages (this is version dependent) +# into a cache location (~/.cache/mill/download). +# +# Mill Project URL: https://github.com/com-lihaoyi/mill +# Script Version: 1.0.0-M1-21-7b6fae-DIRTY892b63e8 +# +# If you want to improve this script, please also contribute your changes back! +# This script was generated from: dist/scripts/src/mill.sh +# +# Licensed under the Apache License, Version 2.0 + +set -e + +if [ "$1" = "--setup-completions" ] ; then + # Need to preserve the first position of those listed options + MILL_FIRST_ARG=$1 + shift +fi + +if [ -z "${DEFAULT_MILL_VERSION}" ] ; then + DEFAULT_MILL_VERSION="0.12.10" +fi + + +if [ -z "${GITHUB_RELEASE_CDN}" ] ; then + GITHUB_RELEASE_CDN="" +fi + + +MILL_REPO_URL="https://github.com/com-lihaoyi/mill" + +if [ -z "${CURL_CMD}" ] ; then + CURL_CMD=curl +fi + +# Explicit commandline argument takes precedence over all other methods +if [ "$1" = "--mill-version" ] ; then + echo "The --mill-version option is no longer supported." 1>&2 +fi + +MILL_BUILD_SCRIPT="" + +if [ -f "build.mill" ] ; then + MILL_BUILD_SCRIPT="build.mill" +elif [ -f "build.mill.scala" ] ; then + MILL_BUILD_SCRIPT="build.mill.scala" +elif [ -f "build.sc" ] ; then + MILL_BUILD_SCRIPT="build.sc" +fi + +# Please note, that if a MILL_VERSION is already set in the environment, +# We reuse it's value and skip searching for a value. + +# If not already set, read .mill-version file +if [ -z "${MILL_VERSION}" ] ; then + if [ -f ".mill-version" ] ; then + MILL_VERSION="$(tr '\r' '\n' < .mill-version | head -n 1 2> /dev/null)" + elif [ -f ".config/mill-version" ] ; then + MILL_VERSION="$(tr '\r' '\n' < .config/mill-version | head -n 1 2> /dev/null)" + elif [ -n "${MILL_BUILD_SCRIPT}" ] ; then + MILL_VERSION="$(cat ${MILL_BUILD_SCRIPT} | grep '//[|] *mill-version: *' | sed 's;//| *mill-version: *;;')" + fi +fi + +MILL_USER_CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/mill" + +if [ -z "${MILL_DOWNLOAD_PATH}" ] ; then + MILL_DOWNLOAD_PATH="${MILL_USER_CACHE_DIR}/download" +fi + +# If not already set, try to fetch newest from Github +if [ -z "${MILL_VERSION}" ] ; then + # TODO: try to load latest version from release page + echo "No mill version specified." 1>&2 + echo "You should provide a version via a '//| mill-version: ' comment or a '.mill-version' file." 1>&2 + + mkdir -p "${MILL_DOWNLOAD_PATH}" + LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" 2>/dev/null || ( + # we might be on OSX or BSD which don't have -d option for touch + # but probably a -A [-][[hh]mm]SS + touch "${MILL_DOWNLOAD_PATH}/.expire_latest"; touch -A -010000 "${MILL_DOWNLOAD_PATH}/.expire_latest" + ) || ( + # in case we still failed, we retry the first touch command with the intention + # to show the (previously suppressed) error message + LANG=C touch -d '1 hour ago' "${MILL_DOWNLOAD_PATH}/.expire_latest" + ) + + # POSIX shell variant of bash's -nt operator, see https://unix.stackexchange.com/a/449744/6993 + # if [ "${MILL_DOWNLOAD_PATH}/.latest" -nt "${MILL_DOWNLOAD_PATH}/.expire_latest" ] ; then + if [ -n "$(find -L "${MILL_DOWNLOAD_PATH}/.latest" -prune -newer "${MILL_DOWNLOAD_PATH}/.expire_latest")" ]; then + # we know a current latest version + MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null) + fi + + if [ -z "${MILL_VERSION}" ] ; then + # we don't know a current latest version + echo "Retrieving latest mill version ..." 1>&2 + LANG=C ${CURL_CMD} -s -i -f -I ${MILL_REPO_URL}/releases/latest 2> /dev/null | grep --ignore-case Location: | sed s'/^.*tag\///' | tr -d '\r\n' > "${MILL_DOWNLOAD_PATH}/.latest" + MILL_VERSION=$(head -n 1 "${MILL_DOWNLOAD_PATH}"/.latest 2> /dev/null) + fi + + if [ -z "${MILL_VERSION}" ] ; then + # Last resort + MILL_VERSION="${DEFAULT_MILL_VERSION}" + echo "Falling back to hardcoded mill version ${MILL_VERSION}" 1>&2 + else + echo "Using mill version ${MILL_VERSION}" 1>&2 + fi +fi + +MILL_NATIVE_SUFFIX="-native" +MILL_JVM_SUFFIX="-jvm" +FULL_MILL_VERSION=$MILL_VERSION +ARTIFACT_SUFFIX="" +set_artifact_suffix(){ + if [ "$(expr substr $(uname -s) 1 5 2>/dev/null)" = "Linux" ]; then + if [ "$(uname -m)" = "aarch64" ]; then + ARTIFACT_SUFFIX="-native-linux-aarch64" + else + ARTIFACT_SUFFIX="-native-linux-amd64" + fi + elif [ "$(uname)" = "Darwin" ]; then + if [ "$(uname -m)" = "arm64" ]; then + ARTIFACT_SUFFIX="-native-mac-aarch64" + else + ARTIFACT_SUFFIX="-native-mac-amd64" + fi + else + echo "This native mill launcher supports only Linux and macOS." 1>&2 + exit 1 + fi +} + +case "$MILL_VERSION" in + *"$MILL_NATIVE_SUFFIX") + MILL_VERSION=${MILL_VERSION%"$MILL_NATIVE_SUFFIX"} + set_artifact_suffix + ;; + + *"$MILL_JVM_SUFFIX") + MILL_VERSION=${MILL_VERSION%"$MILL_JVM_SUFFIX"} + ;; + + *) + case "$MILL_VERSION" in + 0.1.*) ;; + 0.2.*) ;; + 0.3.*) ;; + 0.4.*) ;; + 0.5.*) ;; + 0.6.*) ;; + 0.7.*) ;; + 0.8.*) ;; + 0.9.*) ;; + 0.10.*) ;; + 0.11.*) ;; + 0.12.*) ;; + *) + set_artifact_suffix + esac + ;; +esac + +MILL="${MILL_DOWNLOAD_PATH}/$MILL_VERSION$ARTIFACT_SUFFIX" + +try_to_use_system_mill() { + if [ "$(uname)" != "Linux" ]; then + return 0 + fi + + MILL_IN_PATH="$(command -v mill || true)" + + if [ -z "${MILL_IN_PATH}" ]; then + return 0 + fi + + SYSTEM_MILL_FIRST_TWO_BYTES=$(head --bytes=2 "${MILL_IN_PATH}") + if [ "${SYSTEM_MILL_FIRST_TWO_BYTES}" = "#!" ]; then + # MILL_IN_PATH is (very likely) a shell script and not the mill + # executable, ignore it. + return 0 + fi + + SYSTEM_MILL_PATH=$(readlink -e "${MILL_IN_PATH}") + SYSTEM_MILL_SIZE=$(stat --format=%s "${SYSTEM_MILL_PATH}") + SYSTEM_MILL_MTIME=$(stat --format=%y "${SYSTEM_MILL_PATH}") + + if [ ! -d "${MILL_USER_CACHE_DIR}" ]; then + mkdir -p "${MILL_USER_CACHE_DIR}" + fi + + SYSTEM_MILL_INFO_FILE="${MILL_USER_CACHE_DIR}/system-mill-info" + if [ -f "${SYSTEM_MILL_INFO_FILE}" ]; then + parseSystemMillInfo() { + LINE_NUMBER="${1}" + # Select the line number of the SYSTEM_MILL_INFO_FILE, cut the + # variable definition in that line in two halves and return + # the value, and finally remove the quotes. + sed -n "${LINE_NUMBER}p" "${SYSTEM_MILL_INFO_FILE}" |\ + cut -d= -f2 |\ + sed 's/"\(.*\)"/\1/' + } + + CACHED_SYSTEM_MILL_PATH=$(parseSystemMillInfo 1) + CACHED_SYSTEM_MILL_VERSION=$(parseSystemMillInfo 2) + CACHED_SYSTEM_MILL_SIZE=$(parseSystemMillInfo 3) + CACHED_SYSTEM_MILL_MTIME=$(parseSystemMillInfo 4) + + if [ "${SYSTEM_MILL_PATH}" = "${CACHED_SYSTEM_MILL_PATH}" ] \ + && [ "${SYSTEM_MILL_SIZE}" = "${CACHED_SYSTEM_MILL_SIZE}" ] \ + && [ "${SYSTEM_MILL_MTIME}" = "${CACHED_SYSTEM_MILL_MTIME}" ]; then + if [ "${CACHED_SYSTEM_MILL_VERSION}" = "${MILL_VERSION}" ]; then + MILL="${SYSTEM_MILL_PATH}" + return 0 + else + return 0 + fi + fi + fi + + SYSTEM_MILL_VERSION=$(${SYSTEM_MILL_PATH} --version | head -n1 | sed -n 's/^Mill.*version \(.*\)/\1/p') + + cat < "${SYSTEM_MILL_INFO_FILE}" +CACHED_SYSTEM_MILL_PATH="${SYSTEM_MILL_PATH}" +CACHED_SYSTEM_MILL_VERSION="${SYSTEM_MILL_VERSION}" +CACHED_SYSTEM_MILL_SIZE="${SYSTEM_MILL_SIZE}" +CACHED_SYSTEM_MILL_MTIME="${SYSTEM_MILL_MTIME}" +EOF + + if [ "${SYSTEM_MILL_VERSION}" = "${MILL_VERSION}" ]; then + MILL="${SYSTEM_MILL_PATH}" + fi +} +try_to_use_system_mill + +# If not already downloaded, download it +if [ ! -s "${MILL}" ] || [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then + case $MILL_VERSION in + 0.0.* | 0.1.* | 0.2.* | 0.3.* | 0.4.* ) + DOWNLOAD_SUFFIX="" + DOWNLOAD_FROM_MAVEN=0 + ;; + 0.5.* | 0.6.* | 0.7.* | 0.8.* | 0.9.* | 0.10.* | 0.11.0-M* ) + DOWNLOAD_SUFFIX="-assembly" + DOWNLOAD_FROM_MAVEN=0 + ;; + *) + DOWNLOAD_SUFFIX="-assembly" + DOWNLOAD_FROM_MAVEN=1 + ;; + esac + case $MILL_VERSION in + 0.12.0 | 0.12.1 | 0.12.2 | 0.12.3 | 0.12.4 | 0.12.5 | 0.12.6 | 0.12.7 | 0.12.8 | 0.12.9 | 0.12.10 | 0.12.11 ) + DOWNLOAD_EXT="jar" + ;; + 0.12.* ) + DOWNLOAD_EXT="exe" + ;; + 0.* ) + DOWNLOAD_EXT="jar" + ;; + *) + DOWNLOAD_EXT="exe" + ;; + esac + + DOWNLOAD_FILE=$(mktemp mill.XXXXXX) + if [ "$DOWNLOAD_FROM_MAVEN" = "1" ] ; then + DOWNLOAD_URL="https://repo1.maven.org/maven2/com/lihaoyi/mill-dist${ARTIFACT_SUFFIX}/${MILL_VERSION}/mill-dist${ARTIFACT_SUFFIX}-${MILL_VERSION}.${DOWNLOAD_EXT}" + else + MILL_VERSION_TAG=$(echo "$MILL_VERSION" | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/') + DOWNLOAD_URL="${GITHUB_RELEASE_CDN}${MILL_REPO_URL}/releases/download/${MILL_VERSION_TAG}/${MILL_VERSION}${DOWNLOAD_SUFFIX}" + unset MILL_VERSION_TAG + fi + + if [ "$MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT" = "1" ] ; then + echo $DOWNLOAD_URL + echo $MILL + exit 0 + fi + # TODO: handle command not found + echo "Downloading mill ${MILL_VERSION} from ${DOWNLOAD_URL} ..." 1>&2 + ${CURL_CMD} -f -L -o "${DOWNLOAD_FILE}" "${DOWNLOAD_URL}" + chmod +x "${DOWNLOAD_FILE}" + mkdir -p "${MILL_DOWNLOAD_PATH}" + mv "${DOWNLOAD_FILE}" "${MILL}" + + unset DOWNLOAD_FILE + unset DOWNLOAD_SUFFIX +fi + +if [ -z "$MILL_MAIN_CLI" ] ; then + MILL_MAIN_CLI="${0}" +fi + +MILL_FIRST_ARG="" +if [ "$1" = "--bsp" ] || [ "${1#"-i"}" != "$1" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--no-daemon" ] || [ "$1" = "--repl" ] || [ "$1" = "--help" ] ; then + # Need to preserve the first position of those listed options + MILL_FIRST_ARG=$1 + shift +fi + +unset MILL_DOWNLOAD_PATH +unset MILL_OLD_DOWNLOAD_PATH +unset OLD_MILL +unset MILL_VERSION +unset MILL_REPO_URL + +# -D mill.main.cli is for compatibility with Mill 0.10.9 - 0.13.0-M2 +# We don't quote MILL_FIRST_ARG on purpose, so we can expand the empty value without quotes +# shellcheck disable=SC2086 +exec "${MILL}" $MILL_FIRST_ARG -D "mill.main.cli=${MILL_MAIN_CLI}" "$@" diff --git a/modules/openapi-generator-mill-plugin/mill-test/mill-build/build.mill b/modules/openapi-generator-mill-plugin/mill-test/mill-build/build.mill new file mode 100644 index 000000000000..b4a79ea0e18c --- /dev/null +++ b/modules/openapi-generator-mill-plugin/mill-test/mill-build/build.mill @@ -0,0 +1,17 @@ + +import coursier.LocalRepositories.Dangerous +import coursier.Repositories +import mill.api.Task +import mill.meta.MillBuildRootModule + +object `package` extends MillBuildRootModule { + + override def repositories = Task { + Seq( + // central needed for default deps + Repositories.central.root, + // the previously installed snapshot is in local maven + // see docs on Coursier, why m2 is considered dangerous + Dangerous.maven2Local.root) + } +} \ No newline at end of file diff --git a/modules/openapi-generator-mill-plugin/mill-test/mill.bat b/modules/openapi-generator-mill-plugin/mill-test/mill.bat new file mode 100644 index 000000000000..f36d81229704 --- /dev/null +++ b/modules/openapi-generator-mill-plugin/mill-test/mill.bat @@ -0,0 +1,299 @@ +@echo off + +rem This is a wrapper script, that automatically selects or downloads Mill from Maven Central or GitHub release pages. +rem +rem This script determines the Mill version to use by trying these sources +rem - env-variable `MILL_VERSION` +rem - local file `.mill-version` +rem - local file `.config/mill-version` +rem - `mill-version` from YAML fronmatter of current buildfile +rem - if accessible, find the latest stable version available on Maven Central (https://repo1.maven.org/maven2) +rem - env-variable `DEFAULT_MILL_VERSION` +rem +rem If a version has the suffix '-native' a native binary will be used. +rem If a version has the suffix '-jvm' an executable jar file will be used, requiring an already installed Java runtime. +rem If no such suffix is found, the script will pick a default based on version and platform. +rem +rem Once a version was determined, it tries to use either +rem - a system-installed mill, if found and it's version matches +rem - an already downloaded version under %USERPROFILE%\.mill\download +rem +rem If no working mill version was found on the system, +rem this script downloads a binary file from Maven Central or Github Pages (this is version dependent) +rem into a cache location (%USERPROFILE%\.mill\download). +rem +rem Mill Project URL: https://github.com/com-lihaoyi/mill +rem Script Version: 1.0.0-M1-21-7b6fae-DIRTY892b63e8 +rem +rem If you want to improve this script, please also contribute your changes back! +rem This script was generated from: dist/scripts/src/mill.bat +rem +rem Licensed under the Apache License, Version 2.0 + +rem setlocal seems to be unavailable on Windows 95/98/ME +rem but I don't think we need to support them in 2019 +setlocal enabledelayedexpansion + +if [!DEFAULT_MILL_VERSION!]==[] ( set "DEFAULT_MILL_VERSION=0.12.10" ) + +if [!MILL_GITHUB_RELEASE_CDN!]==[] ( set "MILL_GITHUB_RELEASE_CDN=" ) + +if [!MILL_MAIN_CLI!]==[] ( set "MILL_MAIN_CLI=%~f0" ) + +set "MILL_REPO_URL=https://github.com/com-lihaoyi/mill" + +SET MILL_BUILD_SCRIPT= + +if exist "build.mill" ( + set MILL_BUILD_SCRIPT=build.mill +) else ( + if exist "build.mill.scala" ( + set MILL_BUILD_SCRIPT=build.mill.scala + ) else ( + if exist "build.sc" ( + set MILL_BUILD_SCRIPT=build.sc + ) else ( + rem no-op + ) + ) +) + +if [!MILL_VERSION!]==[] ( + if exist .mill-version ( + set /p MILL_VERSION=<.mill-version + ) else ( + if exist .config\mill-version ( + set /p MILL_VERSION=<.config\mill-version + ) else ( + if not "%MILL_BUILD_SCRIPT%"=="" ( + for /f "tokens=1-2*" %%a in ('findstr /C:"//| mill-version:" %MILL_BUILD_SCRIPT%') do ( + set "MILL_VERSION=%%c" + ) + ) else ( + rem no-op + ) + ) + ) +) + +if [!MILL_VERSION!]==[] set MILL_VERSION=%DEFAULT_MILL_VERSION% + +if [!MILL_DOWNLOAD_PATH!]==[] set MILL_DOWNLOAD_PATH=%USERPROFILE%\.mill\download + +rem without bat file extension, cmd doesn't seem to be able to run it + +set "MILL_NATIVE_SUFFIX=-native" +set "MILL_JVM_SUFFIX=-jvm" +set "FULL_MILL_VERSION=%MILL_VERSION%" +set "MILL_EXT=.bat" +set "ARTIFACT_SUFFIX=" +REM Check if MILL_VERSION contains MILL_NATIVE_SUFFIX +echo !MILL_VERSION! | findstr /C:"%MILL_NATIVE_SUFFIX%" >nul +if !errorlevel! equ 0 ( + set "MILL_VERSION=%MILL_VERSION:-native=%" + REM -native images compiled with graal do not support windows-arm + REM https://github.com/oracle/graal/issues/9215 + IF /I NOT "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( + set "ARTIFACT_SUFFIX=-native-windows-amd64" + set "MILL_EXT=.exe" + ) else ( + rem no-op + ) +) else ( + echo !MILL_VERSION! | findstr /C:"%MILL_JVM_SUFFIX%" >nul + if !errorlevel! equ 0 ( + set "MILL_VERSION=%MILL_VERSION:-jvm=%" + ) else ( + set "SKIP_VERSION=false" + set "MILL_PREFIX=%MILL_VERSION:~0,4%" + if "!MILL_PREFIX!"=="0.1." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.2." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.3." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.4." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.5." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.6." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.7." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.8." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.9." set "SKIP_VERSION=true" + set "MILL_PREFIX=%MILL_VERSION:~0,5%" + if "!MILL_PREFIX!"=="0.10." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.11." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.12." set "SKIP_VERSION=true" + + if "!SKIP_VERSION!"=="false" ( + IF /I NOT "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( + set "ARTIFACT_SUFFIX=-native-windows-amd64" + set "MILL_EXT=.exe" + ) + ) else ( + rem no-op + ) + ) +) + +set MILL=%MILL_DOWNLOAD_PATH%\!FULL_MILL_VERSION!!MILL_EXT! + +set MILL_RESOLVE_DOWNLOAD= + +if not exist "%MILL%" ( + set MILL_RESOLVE_DOWNLOAD=true +) else ( + if defined MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT ( + set MILL_RESOLVE_DOWNLOAD=true + ) else ( + rem no-op + ) +) + + +if [!MILL_RESOLVE_DOWNLOAD!]==[true] ( + set MILL_VERSION_PREFIX=%MILL_VERSION:~0,4% + set MILL_SHORT_VERSION_PREFIX=%MILL_VERSION:~0,2% + rem Since 0.5.0 + set MILL_DOWNLOAD_SUFFIX=-assembly + rem Since 0.11.0 + set MILL_DOWNLOAD_FROM_MAVEN=1 + if [!MILL_VERSION_PREFIX!]==[0.0.] ( + set MILL_DOWNLOAD_SUFFIX= + set MILL_DOWNLOAD_FROM_MAVEN=0 + ) + if [!MILL_VERSION_PREFIX!]==[0.1.] ( + set MILL_DOWNLOAD_SUFFIX= + set MILL_DOWNLOAD_FROM_MAVEN=0 + ) + if [!MILL_VERSION_PREFIX!]==[0.2.] ( + set MILL_DOWNLOAD_SUFFIX= + set MILL_DOWNLOAD_FROM_MAVEN=0 + ) + if [!MILL_VERSION_PREFIX!]==[0.3.] ( + set MILL_DOWNLOAD_SUFFIX= + set MILL_DOWNLOAD_FROM_MAVEN=0 + ) + if [!MILL_VERSION_PREFIX!]==[0.4.] ( + set MILL_DOWNLOAD_SUFFIX= + set MILL_DOWNLOAD_FROM_MAVEN=0 + ) + if [!MILL_VERSION_PREFIX!]==[0.5.] set MILL_DOWNLOAD_FROM_MAVEN=0 + if [!MILL_VERSION_PREFIX!]==[0.6.] set MILL_DOWNLOAD_FROM_MAVEN=0 + if [!MILL_VERSION_PREFIX!]==[0.7.] set MILL_DOWNLOAD_FROM_MAVEN=0 + if [!MILL_VERSION_PREFIX!]==[0.8.] set MILL_DOWNLOAD_FROM_MAVEN=0 + if [!MILL_VERSION_PREFIX!]==[0.9.] set MILL_DOWNLOAD_FROM_MAVEN=0 + + set MILL_VERSION_PREFIX=%MILL_VERSION:~0,5% + if [!MILL_VERSION_PREFIX!]==[0.10.] set MILL_DOWNLOAD_FROM_MAVEN=0 + + set MILL_VERSION_PREFIX=%MILL_VERSION:~0,8% + if [!MILL_VERSION_PREFIX!]==[0.11.0-M] set MILL_DOWNLOAD_FROM_MAVEN=0 + + set MILL_VERSION_PREFIX=%MILL_VERSION:~0,5% + set DOWNLOAD_EXT=exe + if [!MILL_SHORT_VERSION_PREFIX!]==[0.] set DOWNLOAD_EXT=jar + if [!MILL_VERSION_PREFIX!]==[0.12.] set DOWNLOAD_EXT=exe + if [!MILL_VERSION!]==[0.12.0] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.1] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.2] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.3] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.4] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.5] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.6] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.7] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.8] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.9] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.10] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.11] set DOWNLOAD_EXT=jar + + set MILL_VERSION_PREFIX= + set MILL_SHORT_VERSION_PREFIX= + + for /F "delims=- tokens=1" %%A in ("!MILL_VERSION!") do set MILL_VERSION_BASE=%%A + set MILL_VERSION_MILESTONE= + for /F "delims=- tokens=2" %%A in ("!MILL_VERSION!") do set MILL_VERSION_MILESTONE=%%A + set MILL_VERSION_MILESTONE_START=!MILL_VERSION_MILESTONE:~0,1! + if [!MILL_VERSION_MILESTONE_START!]==[M] ( + set MILL_VERSION_TAG=!MILL_VERSION_BASE!-!MILL_VERSION_MILESTONE! + ) else ( + set MILL_VERSION_TAG=!MILL_VERSION_BASE! + ) + if [!MILL_DOWNLOAD_FROM_MAVEN!]==[1] ( + set MILL_DOWNLOAD_URL=https://repo1.maven.org/maven2/com/lihaoyi/mill-dist!ARTIFACT_SUFFIX!/!MILL_VERSION!/mill-dist!ARTIFACT_SUFFIX!-!MILL_VERSION!.!DOWNLOAD_EXT! + ) else ( + set MILL_DOWNLOAD_URL=!MILL_GITHUB_RELEASE_CDN!%MILL_REPO_URL%/releases/download/!MILL_VERSION_TAG!/!MILL_VERSION!!MILL_DOWNLOAD_SUFFIX! + ) + + if defined MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT ( + echo !MILL_DOWNLOAD_URL! + echo !MILL! + exit /b 0 + ) + + rem there seems to be no way to generate a unique temporary file path (on native Windows) + set MILL_DOWNLOAD_FILE=%MILL%.tmp + + echo Downloading mill !MILL_VERSION! from !MILL_DOWNLOAD_URL! ... 1>&2 + + if not exist "%MILL_DOWNLOAD_PATH%" mkdir "%MILL_DOWNLOAD_PATH%" + rem curl is bundled with recent Windows 10 + rem but I don't think we can expect all the users to have it in 2019 + where /Q curl + if !ERRORLEVEL! EQU 0 ( + curl -f -L "!MILL_DOWNLOAD_URL!" -o "!MILL_DOWNLOAD_FILE!" + ) else ( + rem bitsadmin seems to be available on Windows 7 + rem without /dynamic, github returns 403 + rem bitsadmin is sometimes needlessly slow but it looks better with /priority foreground + bitsadmin /transfer millDownloadJob /dynamic /priority foreground "!MILL_DOWNLOAD_URL!" "!MILL_DOWNLOAD_FILE!" + ) + if not exist "!MILL_DOWNLOAD_FILE!" ( + echo Could not download mill !MILL_VERSION! 1>&2 + exit /b 1 + ) + + move /y "!MILL_DOWNLOAD_FILE!" "%MILL%" + + set MILL_DOWNLOAD_FILE= + set MILL_DOWNLOAD_SUFFIX= +) + +set MILL_DOWNLOAD_PATH= +set MILL_VERSION= +set MILL_REPO_URL= + +rem Need to preserve the first position of those listed options +set MILL_FIRST_ARG= +if [%~1%]==[--bsp] ( + set MILL_FIRST_ARG=%1% +) else ( + if [%~1%]==[-i] ( + set MILL_FIRST_ARG=%1% + ) else ( + if [%~1%]==[--interactive] ( + set MILL_FIRST_ARG=%1% + ) else ( + if [%~1%]==[--no-server] ( + set MILL_FIRST_ARG=%1% + ) else ( + if [%~1%]==[--no-daemon] ( + set MILL_FIRST_ARG=%1% + ) else ( + if [%~1%]==[--repl] ( + set MILL_FIRST_ARG=%1% + ) else ( + if [%~1%]==[--help] ( + set MILL_FIRST_ARG=%1% + ) + ) + ) + ) + ) + ) +) +set "MILL_PARAMS=%*%" + +if not [!MILL_FIRST_ARG!]==[] ( + for /f "tokens=1*" %%a in ("%*") do ( + set "MILL_PARAMS=%%b" + ) +) + +rem -D mill.main.cli is for compatibility with Mill 0.10.9 - 0.13.0-M2 +"%MILL%" %MILL_FIRST_ARG% -D "mill.main.cli=%MILL_MAIN_CLI%" %MILL_PARAMS% diff --git a/modules/openapi-generator-mill-plugin/mill-test/src/main/java/com/acme/foo/boundary/web/TestController.java b/modules/openapi-generator-mill-plugin/mill-test/src/main/java/com/acme/foo/boundary/web/TestController.java new file mode 100644 index 000000000000..67907ea03034 --- /dev/null +++ b/modules/openapi-generator-mill-plugin/mill-test/src/main/java/com/acme/foo/boundary/web/TestController.java @@ -0,0 +1,52 @@ +package com.acme.foo.boundary.web; + +import com.acme.foo.boundary.web.api.PetApi; +import com.acme.foo.boundary.web.model.ModelApiResponse; +import com.acme.foo.boundary.web.model.Pet; +import jakarta.enterprise.context.RequestScoped; + +import java.io.InputStream; +import java.util.List; + +@RequestScoped +public class TestController implements PetApi { + @Override + public Pet addPet(Pet pet) { + return null; + } + + @Override + public void deletePet(Long petId, String apiKey) { + + } + + @Override + public List findPetsByStatus(List status) { + return List.of(); + } + + @Override + public List findPetsByTags(List tags) { + return List.of(); + } + + @Override + public Pet getPetById(Long petId) { + return null; + } + + @Override + public Pet updatePet(Pet pet) { + return null; + } + + @Override + public void updatePetWithForm(Long petId, String name, String status) { + + } + + @Override + public ModelApiResponse uploadFile(Long petId, String additionalMetadata, InputStream _fileInputStream) { + return null; + } +} diff --git a/modules/openapi-generator-mill-plugin/mill-test/version b/modules/openapi-generator-mill-plugin/mill-test/version new file mode 100644 index 000000000000..653dba67d0cc --- /dev/null +++ b/modules/openapi-generator-mill-plugin/mill-test/version @@ -0,0 +1 @@ +7.16.0-SNAPSHOT \ No newline at end of file diff --git a/modules/openapi-generator-mill-plugin/pom.xml b/modules/openapi-generator-mill-plugin/pom.xml new file mode 100644 index 000000000000..c94724084f4f --- /dev/null +++ b/modules/openapi-generator-mill-plugin/pom.xml @@ -0,0 +1,180 @@ + + + org.openapitools + openapi-generator-project + + 7.19.0-SNAPSHOT + + ../../pom.xml + + 4.0.0 + + openapi-generator-mill-plugin + jar + openapi-generator-mill-plugin + Mill module to build modules from OpenAPI Generator + + + + + + central + Central Repository OSSRH + https://repo1.maven.org/maven2/ + + + + + 4.9.5 + 3.7.4 + 1.0.6 + + + + + org.scala-lang + scala3-library_3 + ${scala.version} + + + com.lihaoyi + mill-libs_3 + ${mill.version} + + + org.openapitools + openapi-generator + ${project.version} + + + com.lihaoyi + mill-testkit_3 + ${mill.version} + test + + + + org.scalatest + scalatest_3 + 3.2.19 + test + + + org.scalatestplus + testng-7-10_3 + 3.2.19.0 + test + + + + org.virtuslab.scala-cli + config_3 + 1.11.0 + test + + + io.github.alexarchambault + concurrent-reference-hash-map + 1.1.0 + test + + + + + src/main/scala + src/test/scala + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + ${project.parent.basedir}${file.separator}google_checkstyle.xml + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.5.0 + + + add-scala-sources + generate-sources + + add-source + + + + src/main/scala + + + + + add-scala-test-sources + generate-test-sources + + add-test-source + + + + src/test/java + src/test/scala + + + + + + + net.alchim31.maven + scala-maven-plugin + ${scala-plugin.version} + + ${scala.version} + + + + + compile + testCompile + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${project.basedir}/src/test/resources + + + + + + + + + static-analysis + + + + com.github.spotbugs + spotbugs-maven-plugin + + ${project.parent.basedir}${file.separator}spotbugs-exclude.xml + + + + org.apache.maven.plugins + maven-pmd-plugin + + + se.bjurr.violations + violations-maven-plugin + + + + + + diff --git a/modules/openapi-generator-mill-plugin/src/main/scala/org/openapitools/generator/mill/OpenApiModule.scala b/modules/openapi-generator-mill-plugin/src/main/scala/org/openapitools/generator/mill/OpenApiModule.scala new file mode 100644 index 000000000000..cc790f8f1b51 --- /dev/null +++ b/modules/openapi-generator-mill-plugin/src/main/scala/org/openapitools/generator/mill/OpenApiModule.scala @@ -0,0 +1,624 @@ +/* + * Original code copied from https://github.com/mikybars/openapi-generator-mill-plugin + * Original code published under the MIT License + * Original Copyright Miguel Ibars + */ +package org.openapitools.generator.mill + +import io.swagger.parser.OpenAPIParser +import io.swagger.v3.parser.core.models.ParseOptions +import mainargs.arg +import mill.T +import mill.api.{PathRef, Result, Task} +import org.openapitools.codegen.CodegenConstants +import org.openapitools.codegen.DefaultGenerator +import org.openapitools.codegen.config.CodegenConfigurator +import org.openapitools.codegen.config.GlobalSettings +import org.openapitools.codegen.validations.oas.{OpenApiEvaluator, RuleConfiguration} +import os.{Path, RelPath} +import upickle.ReadWriter as RW + +import scala.jdk.CollectionConverters.* +import scala.jdk.javaapi.CollectionConverters + +/** + * Usage: + * {{{ + * object myModule extends JavaModule with OpenApiModule { + * + * object openApiServer extends OpenApiConfig { + * def inputSpec: T[PathRef] = Task.Source(BuildCtx.workspaceRoot / "api" / "server-api.yaml") + * def apiPackage: T[String] = "com.acme.foo.boundary.web.api" + * def modelPackage: T[String] = "com.acme.foo.boundary.web.model" + * def generatorName: T[String] = "spring" + * def sourceFolder: T[String] = "src/main/java" + * def additionalProperties: T[Map[String, String]] = Map( + * "useSpringBoot3" -> "true", + * "dateLibrary" -> "java8", + * "interfaceOnly" -> "true", + * "performBeanValidation" -> "true", + * "useBeanValidation" -> "false", + * "skipDefaultInterface" -> "true", + * "useTags" -> "true", + * ) + * } + * + * object openApiClient extends OpenApiConfig { + * def inputSpec = T[PathRef] = Task.Source(BuildCtx.workspaceRoot / "api" / "some-client-api.yaml") + * def apiPackage: T[String] = "com.acme.foo.boundary.client.some.api" + * def modelPackage: T[String] = "com.acme.foo.boundary.client.some.model" + * def generatorName: T[String] = "java" + * def modelNameSuffix: T[String] = "Dto" + * def sourceFolder: T[String] = "src/main/java" + * def additionalProperties: T[Map[String, String]] = Map( + * "useTags" -> "true", + * "dateLibrary" -> "java8", + * "library" -> "webclient", + * "useJakartaEe" -> "true", + * "useOneOfInterfaces" -> "true", + * "useAbstractionForFiles" -> "true", + * ) + * } + * + * override def generatedSources: T[Seq[PathRef]] = Seq( + * PathRef(Task.dest), + * openApiServer.generate(), + * openApiClient.generate(), + * ) + * } + * }}} + */ +trait OpenApiModule extends mill.api.Module { + + trait OpenApiConfig extends mill.api.Module { + + case class ArtifactSettings( + groupId: String, + artifactId: String, + artifactVersion: Option[String] = None + ) derives RW + + case class GitSettings( + host: String, + userId: String, + repoId: String + ) derives RW + + + /** The Open API 2.0/3.x specification location. */ + def inputSpec: T[PathRef] + + /** The name of the generator which will handle codegen. */ + def generatorName: T[String] + + /** Package for generated api classes. */ + def apiPackage: T[String] + + /** + * The additional folder passed to the generator. Usually this is done via the [[additionalProperties]] but since + * some generators also use different defaults while others use none, this property needs to be set and will + * override anything set in [[additionalProperties]]. + * + * This is necessary so the plugin can construct the correct source folder structure. + */ + def sourceFolder: T[String] + + /** + * Suffix that will be appended to all api names. Default is the empty string. + */ + def apiNameSuffix: T[String] = "" + + /** + * Adds authorization headers when fetching the OpenAPI definitions remotely. + * Pass in a URL-encoded string of name:header with a comma separating multiple values + */ + def auth: T[Option[String]] = None + + /** + * Sets custom User-Agent header value + */ + def httpUserAgent: T[Option[String]] = None + + /** Package for generated model classes. */ + def modelPackage: T[String] + + /** + * Prefix that will be prepended to all model names. Default is the empty string. + */ + def modelNamePrefix: T[String] = "" + + /** + * Suffix that will be appended to all model names. Default is the empty string. + */ + def modelNameSuffix: T[String] = "" + + /** Sets additional properties that can be referenced by the mustache templates. */ + def additionalProperties: T[Map[String, String]] = Map.empty[String, String] + + /** + * Defines the user's target type. + * {{{ "OffsetDateTime" -> "java.time.Instant" }}} + * + * @see [[https://openapi-generator.tech/docs/usage/#type-mappings-and-import-mappings]] + * */ + def typeMappings: T[Map[String, String]] = Map.empty[String, String] + + /** + * Informs the template of the type to be imported. Needed when type mappings are used. + * {{{ "OffsetDateTime" -> "java.time.Instant" }}} + * Since the [[typeMappings]] are used to change the default types, the import mappings are used to map the imports. + * + * @see [[https://openapi-generator.tech/docs/usage/#type-mappings-and-import-mappings]] + * */ + def importMappings: T[Map[String, String]] = Map.empty[String, String] + + /** + * Specifies mappings between a given schema and the new one. + */ + def schemaMappings: T[Map[String, String]] = Map.empty[String, String] + + /** Specify if the spec should be validated. Default is true. */ + def validateSpec: T[Boolean] = true + + /** + * Specifies an override location for the .openapi-generator-ignore file. + */ + def ignoreFileOverride: T[Option[Path]] = None + + /** + * Specifies how a reserved name should be escaped to. + */ + def reservedWordsMappings: T[Map[String, String]] = Map.empty[String, String] + + /** + * Remove examples defined in the operation + */ + def skipOperationExample: T[Boolean] = false + + /** + * Defines which API-related files should be generated. This allows you to create a subset of generated files (or none at all). + * + * This option enables/disables generation of ALL api-related files. + * + * NOTE: Configuring any one of [[apiFilesConstrainedTo]], [[modelFilesConstrainedTo]], or [[supportingFilesConstrainedTo]] results + * in others being disabled. That is, OpenAPI Generator considers any one of these to define a subset of generation. + * For more control over the generation of individual files, configure an ignored file and refer to it via [[ignoreFileOverride]]. + */ + def apiFilesConstrainedTo: T[Seq[String]] = Seq.empty[String] + + /** + * Defines which model-related files should be generated. This allows you to create a subset of generated files (or none at all). + * + * NOTE: Configuring any one of [[apiFilesConstrainedTo]], [[modelFilesConstrainedTo]], or [[supportingFilesConstrainedTo]] results + * in others being disabled. That is, OpenAPI Generator considers any one of these to define a subset of generation. + * For more control over the generation of individual files, configure an ignored file and refer to it via [[ignoreFileOverride]]. + */ + def modelFilesConstrainedTo: T[Seq[String]] = Seq.empty[String] + + /** + * Defines which supporting files should be generated. This allows you to create a subset of generated files (or none at all). + * + * Supporting files are those related to `projects/frameworks` which may be modified + * by consumers. + * + * NOTE: Configuring any one of [[apiFilesConstrainedTo]], [[modelFilesConstrainedTo]], or [[supportingFilesConstrainedTo]] results + * in others being disabled. That is, OpenAPI Generator considers any one of these to define a subset of generation. + * For more control over the generation of individual files, configure an ignored file and refer to it via [[ignoreFileOverride]]. + */ + def supportingFilesConstrainedTo: T[Seq[String]] = Seq.empty[String] + + /** + * Generate the APIs. Default is true. + */ + def generateApis: T[Boolean] = true + + /** + * Defines whether api-related _documentation_ files should be generated. + * + * This option enables/disables generation of ALL api-related _documentation_ files. + * + * For more control over generation of individual files, configure an ignored file and + * refer to it via [[ignoreFileOverride]]. + */ + def generateApiDocs: T[Boolean] = true + + /** + * Defines whether api-related _test_ files should be generated. + * + * This option is currently disabled because Mill does not distinguish between normal- and test-sources. + */ + // TODO figure out a clean way to support this + final def generateApiTests: T[Boolean] = false + + /** + * Generate the Models. Default is true. + */ + def generateModels: T[Boolean] = true + + /** + * Defines whether model-related _documentation_ files should be generated. + * + * This option enables/disables generation of ALL model-related _documentation_ files. + * + * For more control over generation of individual files, configure an ignored file and + * refer to it via [[ignoreFileOverride]]. + */ + def generateModelDocs: T[Boolean] = true + + /** + * Defines whether model-related _test_ files should be generated. + * + * This option is currently disabled because Mill does not distinguish between normal- and test-sources. + */ + // TODO figure out a clean way to support this + def generateModelTests: T[Boolean] = false + + /** + * Generate the supporting files. Default is true. + */ + def generateSupportingFiles: T[Boolean] = true + + /** + * Generate the models recursively if models should generate selectively (see [[modelFilesConstrainedTo]]) and all + * dependent models are to generate. + * Is enabled by default when [[modelFilesConstrainedTo]] is not empty for convenience. + */ + def generateRecursiveDependentModels: T[Boolean] = modelFilesConstrainedTo().nonEmpty + + /** + * Templating engine: "mustache" (default) or "handlebars" (beta) + */ + def engine: T[Option[String]] = None + + /** + * Specifies mappings between the inline scheme name and the new name + */ + def inlineSchemaNameMappings: T[Map[String, String]] = Map.empty[String, String] + + /** + * Specifies options for inline schemas + */ + def inlineSchemaOptions: T[Map[String, String]] = Map.empty[String, String] + + /** + * Specifies mappings between the property name and the new name + */ + def nameMappings: T[Map[String, String]] = Map.empty[String, String] + + /** + * Specifies mappings between the parameter name and the new name + */ + def parameterNameMappings: T[Map[String, String]] = Map.empty[String, String] + + /** + * Specifies mappings between the model name and the new name + */ + def modelNameMappings: T[Map[String, String]] = Map.empty[String, String] + + /** + * Specifies mappings between the enum name and the new name + */ + def enumNameMappings: T[Map[String, String]] = Map.empty[String, String] + + /** + * Specifies mappings between the operation id name and the new name + */ + def operationIdNameMappings: T[Map[String, String]] = Map.empty[String, String] + + /** + * Specifies mappings (rules) in OpenAPI normalizer + */ + def openapiNormalizer: T[Map[String, String]] = Map.empty[String, String] + + /** + * Root package for generated code. + */ + def invokerPackage: T[Option[String]] = None + + /** + * Artifact coordinates/packages used in generated build files. + */ + def artifactSettings: T[Option[ArtifactSettings]] = None + + /** + * Reference the library template (sub-template) of a generator. + */ + def library: T[Option[String]] = None + + /** + * To write all log messages (not just errors) to STDOUT + */ + def logToStderr: T[Boolean] = false + + /** + * To enable the file post-processing hook. This enables executing an external post-processor (usually a linter program). + * This only enables the post-processor. To define the post-processing command, define an environment variable such as + * LANG_POST_PROCESS_FILE (e.g. GO_POST_PROCESS_FILE, SCALA_POST_PROCESS_FILE). Please open an issue if your target + * generator does not support this functionality. + */ + def enablePostProcessFile: T[Boolean] = false + + /** + * Folder containing the template files. + */ + def templateDirectory: T[Option[Path]] = None + + /** + * To remove operationId prefix (e.g. user_getName => getName) + */ + def removeOperationIdPrefix: T[Boolean] = false + + /** + * To treat a document strictly against the spec. Default is true. + */ + def strictSpec: T[Boolean] = true + + /** + * Specifies additional language-specific primitive types in the format of type1,type2,type3,type3. For example, `String,boolean,Boolean,Double`. + */ + def languageSpecificPrimitives: T[Set[String]] = Set.empty[String] + + /** + * Git repository used in generated documentation. + */ + def gitSettings: T[Option[GitSettings]] = None + + /** + * Sets specified global properties. + */ + def globalProperties: T[Map[String, String]] = Map.empty[String, String] + /** + * To skip spec validation. When true, we will skip the default behavior of validating a spec before generation. + */ + def skipValidateSpec: T[Boolean] = false + + /** + * To generate alias (array, list, map) as model. When false, top-level objects defined as array, list, or map will result in those + * definitions generated as top-level Array-of-items, List-of-items, Map-of-items definitions. + * When true, A model representation either containing or extending the array,list,map (depending on specific generator implementation) will be generated. + */ + def generateAliasAsModel: T[Boolean] = false + + /** + * Defines whether the output dir should be cleaned up before generating the output. + */ + def cleanupOutput: T[Boolean] = false + + /** + * Specifies if the existing files should be overwritten during the generation. + */ + def skipOverwrite: T[Boolean] = false + + /** + * Defines whether the generator should run in dry-run mode. + */ + def dryRun: T[Boolean] = false + + + /** + * An additional Task, which can be run after the generation phase. + * + * For instance, in case you have a shared type which is defined in your importMappings, + * the generator will still generate the type even though it is not used. The following + * cleanup will remove all files which are are not used. + * {{{ + * override def cleanup: Task[Option[Path => Unit]] = Task.Anon { + * Some( + * (path: Path) => { + * val filenames = (for { + * (_, fqn) <- importMappings() + * if fqn.startsWith("my.relevant.package.") + * } yield fqn.split('.').last).toSeq + * + * os.walk(path) + * .filter { p => os.isFile(p) && filenames.contains(p.baseName) } + * .foreach { p => os.remove(p) } + * }) + * } + * }}} + * This will match all full-qualified-names against the provided package and delete the matches. + * Note: this sample requires that custom type packages are completely separate. + */ + def cleanup: Task[Option[Path => Unit]] = Task.Anon { + None + } + + /** + * Runs the OpenAPI generator with the given configuration. + */ + def generate: T[PathRef] = Task { + val configurator = CodegenConfigurator() + // don't call setAdditionalProperties with an immutable Scala Map, because the Setters after this one + // might add to the attributes as well (which will then cause an Exception) + additionalProperties().foreach((k, v) => configurator.addAdditionalProperty(k, v)) + + configurator.setApiNameSuffix(apiNameSuffix()) + .setApiPackage(apiPackage()) + .setEnumNameMappings(enumNameMappings().asJava) + .setGeneratorName(generatorName()) + .setGlobalProperties(globalProperties().asJava) + .setInputSpec(inputSpec().path.toString()) + .setInlineSchemaNameMappings(inlineSchemaNameMappings().asJava) + .setInlineSchemaOptions(inlineSchemaOptions().asJava) + .setImportMappings(importMappings().asJava) + .setLanguageSpecificPrimitives(languageSpecificPrimitives().asJava) + .setModelNameMappings(modelNameMappings().asJava) + .setModelNamePrefix(modelNamePrefix()) + .setModelNameSuffix(modelNameSuffix()) + .setModelPackage(modelPackage()) + .setNameMappings(nameMappings().asJava) + .setOperationIdNameMappings(operationIdNameMappings().asJava) + .setOpenapiNormalizer(openapiNormalizer().asJava) + // should output-dir be configurable like in Gradle (don't think so) + .setOutputDir(Task.dest.toString()) + .setParameterNameMappings(parameterNameMappings().asJava) + .setReservedWordsMappings(reservedWordsMappings().asJava) + .setSchemaMappings(schemaMappings().asJava) + .setTypeMappings(typeMappings().asJava) + .setValidateSpec(validateSpec()) + + if(generateApis()){ + GlobalSettings.setProperty(CodegenConstants.APIS, apiFilesConstrainedTo().mkString(",")) + } + if(generateModels()){ + GlobalSettings.setProperty(CodegenConstants.MODELS, modelFilesConstrainedTo().mkString(",")) + } + GlobalSettings.setProperty(CodegenConstants.GENERATE_RECURSIVE_DEPENDENT_MODELS, generateRecursiveDependentModels().toString) + if(generateSupportingFiles()){ + GlobalSettings.setProperty(CodegenConstants.SUPPORTING_FILES, supportingFilesConstrainedTo().mkString(",")) + } + + GlobalSettings.setProperty(CodegenConstants.API_DOCS, generateApiDocs().toString) + GlobalSettings.setProperty(CodegenConstants.API_TESTS, generateApiTests().toString) + GlobalSettings.setProperty(CodegenConstants.MODEL_DOCS, generateModelDocs().toString) + GlobalSettings.setProperty(CodegenConstants.MODEL_TESTS, generateModelTests().toString) + + engine() match { + case Some(s) if s.equalsIgnoreCase("handlebars") => configurator.setTemplatingEngineName("handlebars") + case Some(s) => configurator.setTemplatingEngineName(s) // in case other engines are supported + case None => () // use default + } + + auth().filter(_.nonEmpty).foreach(authUrl => configurator.setAuth(authUrl)) + httpUserAgent().foreach(userAgent => configurator.setHttpUserAgent(userAgent)) + ignoreFileOverride().foreach(file => configurator.setIgnoreFileOverride(file.toNIO.toAbsolutePath.toString)) + invokerPackage().foreach(value => configurator.setInvokerPackage(value)) + artifactSettings().foreach(settings => + configurator.setGroupId(settings.groupId) + configurator.setArtifactId(settings.artifactId) + settings.artifactVersion.foreach(value => configurator.setArtifactVersion(value)) + ) + library().foreach(value => configurator.setLibrary(value)) + templateDirectory().foreach(file => configurator.setTemplateDir(file.toNIO.toAbsolutePath.toString)) + gitSettings().foreach(git => + configurator.setGitHost(git.host) + configurator.setGitUserId(git.userId) + configurator.setGitRepoId(git.repoId) + ) + if (logToStderr()) { + configurator.setLogToStderr(true) + } + if (enablePostProcessFile()) { + configurator.setEnablePostProcessFile(true) + } + if (skipValidateSpec()) { + configurator.setValidateSpec(false) + } + if (generateAliasAsModel()) { + configurator.setGenerateAliasAsModel(true) + } + if (removeOperationIdPrefix()) { + configurator.setRemoveOperationIdPrefix(true) + } + if (skipOperationExample()) { + configurator.setSkipOperationExample(true) + } + if (strictSpec()) { + configurator.setStrictSpecBehavior(true) + } + if (skipOverwrite()) { + configurator.setSkipOverwrite(true) + } + + if (cleanupOutput()) { + os.remove.all(Task.dest) + os.makeDir.all(Task.dest) + Task.log.info(s"Cleaned up output directory ${Task.dest} before code generation (cleanupOutput set to true).") + } + val dryRunSetting = dryRun() + + // set source-folder as last to override potential duplicate + configurator.addAdditionalProperty("sourceFolder", sourceFolder()) + + DefaultGenerator(dryRunSetting).opts(configurator.toClientOptInput).generate() + Task.log.info(s"Successfully generated code to ${Task.dest}") + cleanup() match { + case Some(f) => f(Task.dest) + case None => // no-op + } + + PathRef(Task.dest / RelPath(sourceFolder())) + } + + /** + * Validates currently configured [[inputSpec]]. This task outputs a list of validation issues and errors. + * + * @param recommend prints warnings for recommended fixes. Default is true. + * @param failOnWarnings fails the call when there are warnings. Default is false. + * @return + */ + def validate( + recommend: Boolean = true, + failOnWarnings: Boolean = false + ): Task.Command[Unit] = Task.Command { + given log: mill.api.Logger = Task.log + runValidation(inputSpec().path, recommend, failOnWarnings) + } + } + + /** + * Command which checks a passed OpenAPI definition. + * This task outputs a list of validation issues and errors. + * + * @param spec the path to the file to be validated. + * @param recommend prints warnings for recommended fixes. Default is true. + * @param failOnWarnings fails the call when there are warnings. Default is false. + */ + def validateOpenapiSpec( + @arg(positional = true) + spec: String, + recommend: Boolean = true, + failOnWarnings: Boolean = false + ): Task.Command[Unit] = Task.Command { + given log: mill.api.Logger = Task.log + runValidation(os.Path(spec), recommend, failOnWarnings) + } + + private def runValidation(spec: Path, recommend: Boolean, failOnWarnings: Boolean)(using log: mill.api.Logger) = { + log.info(s"Validating spec $spec") + + val options = ParseOptions() + options.setResolve(true) + + val result = OpenAPIParser().readLocation(spec.toNIO.toAbsolutePath.toString, null, options) + val messages = CollectionConverters.asScala(result.getMessages).toSet + + val ruleConfiguration = RuleConfiguration() + ruleConfiguration.setEnableRecommendations(recommend) + + val evaluator = OpenApiEvaluator(ruleConfiguration) + val validationResult = evaluator.validate(result.getOpenAPI) + val warnings = CollectionConverters.asScala(validationResult.getWarnings) + val errors = CollectionConverters.asScala(validationResult.getErrors) + + if (warnings.nonEmpty) { + val sb = StringBuilder("Spec has issues or recommendations.\nIssues:\n") + warnings.foreach(w => { + sb.append(s"\t${w.getMessage}\n") + log.debug(s"WARNING: ${w.getMessage}|${w.getDetails}") + }) + log.info(sb.toString()) + } + + if (messages.nonEmpty || errors.nonEmpty) { + val sb = new StringBuilder("Spec is invalid.\nIssues:\n") + + messages.foreach(m => { + sb.append(s"\t$m\n") + log.debug(s"ERROR: $m") + }) + errors.foreach(e => { + sb.append(s"\t$e\n") + log.debug(s"ERROR: ${e.getMessage}|${e.getDetails}") + }) + log.error(sb.toString()) + Result.Failure("Validation failed.") + } else if (failOnWarnings && warnings.nonEmpty) { + log.error("Warnings found in the spec and 'treatWarningsAsErrors' is enabled.\nFailing validation.\n") + Result.Failure("Validation failed due to warnings (treatWarningsAsErrors = true).") + } else { + log.info("No error validations from swagger-parser or internal validations.") + Result.Success("Spec is valid.") + } + } +} diff --git a/modules/openapi-generator-mill-plugin/src/test/resources/specs/petstore-v3.0-invalid-due-to-missing-info-attribute.yaml b/modules/openapi-generator-mill-plugin/src/test/resources/specs/petstore-v3.0-invalid-due-to-missing-info-attribute.yaml new file mode 100644 index 000000000000..0f5c6fc29829 --- /dev/null +++ b/modules/openapi-generator-mill-plugin/src/test/resources/specs/petstore-v3.0-invalid-due-to-missing-info-attribute.yaml @@ -0,0 +1,103 @@ +openapi: "3.0.0" +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/modules/openapi-generator-mill-plugin/src/test/resources/specs/petstore-v3.1.yaml b/modules/openapi-generator-mill-plugin/src/test/resources/specs/petstore-v3.1.yaml new file mode 100644 index 000000000000..0a8d3d15973c --- /dev/null +++ b/modules/openapi-generator-mill-plugin/src/test/resources/specs/petstore-v3.1.yaml @@ -0,0 +1,109 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /v3/pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /v3/pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/modules/openapi-generator-mill-plugin/src/test/scala/org/openapitools/generator/mill/MillOpenapiModuleTest.scala b/modules/openapi-generator-mill-plugin/src/test/scala/org/openapitools/generator/mill/MillOpenapiModuleTest.scala new file mode 100644 index 000000000000..a0ff5a77ff31 --- /dev/null +++ b/modules/openapi-generator-mill-plugin/src/test/scala/org/openapitools/generator/mill/MillOpenapiModuleTest.scala @@ -0,0 +1,109 @@ +package org.openapitools.generator.mill + +import mill.* +import mill.api.{BuildCtx, Discover} +import mill.javalib.{Dep, JavaModule} +import mill.testkit.{TestRootModule, UnitTester} +import org.scalatest.matchers.should.Matchers +import org.testng.annotations.Test +import scala.language.postfixOps + +object MillOpenapiModuleTestRoot extends TestRootModule { + + lazy val millDiscover = Discover[this.type] + + /** a project definition for Mill which should generate and compile */ + object petstoreMicroprofile extends JavaModule with OpenApiModule { + + override def mvnDeps: T[Seq[Dep]] = Seq( + Dep.parse("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0"), + Dep.parse("jakarta.json.bind:jakarta.json.bind-api:3.0.0"), + Dep.parse("jakarta.json:jakarta.json-api:2.1.0"), + Dep.parse("org.eclipse.microprofile.rest.client:microprofile-rest-client-api:3.0.1"), + ) + + object openapi extends OpenApiConfig { + + override def inputSpec: T[PathRef] = Task.Source(BuildCtx.workspaceRoot / "petstore-v3.1.yaml") + override def generatorName: T[String] = "java-microprofile" + override def apiPackage: T[String] = "com.acme.foo.boundary.web.api" + override def modelPackage: T[String] = "com.acme.foo.boundary.web.model" + override def sourceFolder: T[String] = "src/gen/java" + override def additionalProperties: T[Map[String, String]] = Map( + "microprofileRestClientVersion" -> "3.0", + "library" -> "microprofile", + "dateLibrary" -> "java8", + "interfaceOnly" -> "true", + "performBeanValidation" -> "true", + "useBeanValidation" -> "false", + "skipDefaultInterface" -> "true", + "useTags" -> "true", + ) + } + + override def generatedSources: T[Seq[PathRef]] = Seq( + PathRef(Task.dest), + openapi.generate(), + ) + } + + /** a project definition for Mill with an invalid openapi-spec */ + object petstoreInvalid extends JavaModule with OpenApiModule { + + override def mvnDeps: T[Seq[Dep]] = Seq( + Dep.parse("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0"), + Dep.parse("jakarta.json.bind:jakarta.json.bind-api:3.0.0"), + Dep.parse("jakarta.json:jakarta.json-api:2.1.0"), + Dep.parse("org.eclipse.microprofile.rest.client:microprofile-rest-client-api:3.0.1"), + ) + + object openapi extends OpenApiConfig { + + override def inputSpec: T[PathRef] = Task.Source(BuildCtx.workspaceRoot / "petstore-v3.0-invalid-due-to-missing-info-attribute.yaml") + override def generatorName: T[String] = "java-microprofile" + override def apiPackage: T[String] = "com.acme.foo.boundary.web.api" + override def modelPackage: T[String] = "com.acme.foo.boundary.web.model" + override def sourceFolder: T[String] = "src/gen/java" + override def additionalProperties: T[Map[String, String]] = Map( + "microprofileRestClientVersion" -> "3.0", + "library" -> "microprofile", + "dateLibrary" -> "java8", + "interfaceOnly" -> "true", + "performBeanValidation" -> "true", + "useBeanValidation" -> "false", + "skipDefaultInterface" -> "true", + "useTags" -> "true", + ) + } + + override def generatedSources: T[Seq[PathRef]] = Seq( + PathRef(Task.dest), + openapi.generate(), + ) + } +} + +class MillOpenapiModuleTest extends Matchers { + + private val resourcePath = os.Path(sys.env("MILL_TEST_RESOURCE_DIR")) / "specs" + private def testEval() = UnitTester(MillOpenapiModuleTestRoot, resourcePath) + + @Test + def petstoreMicroprofileGeneratesAndCompiles(): Unit = { + val result = testEval().scoped{ eval => + // execute 'compile` task + eval.apply(MillOpenapiModuleTestRoot.petstoreMicroprofile.compile) + } + + result shouldBe a[Right[_, _]] + } + + @Test + def petstoreMicroprofileInvalidSpec(): Unit = { + val result = testEval().scoped { eval => + // execute 'compile` task + eval.apply(MillOpenapiModuleTestRoot.petstoreInvalid.compile) + } + result shouldBe a[Left[_, _]] + } +} diff --git a/pom.xml b/pom.xml index 2af6224d9333..567410b7eb00 100644 --- a/pom.xml +++ b/pom.xml @@ -1164,6 +1164,7 @@ modules/openapi-generator-cli modules/openapi-generator-maven-plugin modules/openapi-generator-gradle-plugin + modules/openapi-generator-mill-plugin modules/openapi-generator-online