add nullable support (#873)

This commit is contained in:
William Cheng 2018-08-23 10:51:57 +08:00 committed by GitHub
parent 3d06900e68
commit 6a47d49ee8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 843 additions and 25 deletions

View File

@ -59,7 +59,9 @@ public class CodegenProperty implements Cloneable {
public boolean isString, isNumeric, isInteger, isLong, isNumber, isFloat, isDouble, isByteArray, isBinary, isFile, isBoolean, isDate, isDateTime, isUuid;
public boolean isListContainer, isMapContainer;
public boolean isEnum;
public boolean isReadOnly = false;
public boolean isReadOnly;
public boolean isWriteOnly;
public boolean isNullable;
public List<String> _enum;
public Map<String, Object> allowableValues;
public CodegenProperty items;
@ -437,6 +439,8 @@ public class CodegenProperty implements Cloneable {
result = prime * result + ((isNotContainer ? 13:31));
result = prime * result + ((isPrimitiveType ? 13:31));
result = prime * result + ((isReadOnly ? 13:31));
result = prime * result + ((isWriteOnly ? 13:31));
result = prime * result + ((isNullable ? 13:31));
result = prime * result + ((items == null) ? 0 : items.hashCode());
result = prime * result + ((mostInnerItems == null) ? 0 : mostInnerItems.hashCode());
result = prime * result + ((jsonSchema == null) ? 0 : jsonSchema.hashCode());
@ -587,6 +591,12 @@ public class CodegenProperty implements Cloneable {
if (this.isReadOnly != other.isReadOnly) {
return false;
}
if (this.isWriteOnly != other.isWriteOnly) {
return false;
}
if (this.isNullable != other.isNullable) {
return false;
}
if (this._enum != other._enum && (this._enum == null || !this._enum.equals(other._enum))) {
return false;
}
@ -769,6 +779,8 @@ public class CodegenProperty implements Cloneable {
", isMapContainer=" + isMapContainer +
", isEnum=" + isEnum +
", isReadOnly=" + isReadOnly +
", isWriteOnly=" + isWriteOnly+
", isNullable=" + isNullable +
", _enum=" + _enum +
", allowableValues=" + allowableValues +
", items=" + items +

View File

@ -185,7 +185,7 @@ public class DefaultCodegen implements CodegenConfig {
.get(CodegenConstants.REMOVE_OPERATION_ID_PREFIX).toString()));
}
if (additionalProperties.containsKey(CodegenConstants.DOCEXTENSION)){
if (additionalProperties.containsKey(CodegenConstants.DOCEXTENSION)) {
this.setDocExtension(String.valueOf(additionalProperties
.get(CodegenConstants.DOCEXTENSION).toString()));
}
@ -771,7 +771,7 @@ public class DefaultCodegen implements CodegenConfig {
public String toVarName(String name) {
if (reservedWords.contains(name)) {
return escapeReservedWord(name);
} else if (((CharSequence) name).chars().anyMatch(character -> specialCharReplacements.keySet().contains( "" + ((char) character)))) {
} else if (((CharSequence) name).chars().anyMatch(character -> specialCharReplacements.keySet().contains("" + ((char) character)))) {
return escapeSpecialCharacters(name, null, null);
} else {
return name;
@ -789,7 +789,7 @@ public class DefaultCodegen implements CodegenConfig {
name = removeNonNameElementToCamelCase(name); // FIXME: a parameter should not be assigned. Also declare the methods parameters as 'final'.
if (reservedWords.contains(name)) {
return escapeReservedWord(name);
} else if (((CharSequence) name).chars().anyMatch(character -> specialCharReplacements.keySet().contains( "" + ((char) character)))) {
} else if (((CharSequence) name).chars().anyMatch(character -> specialCharReplacements.keySet().contains("" + ((char) character)))) {
return escapeSpecialCharacters(name, null, null);
}
return name;
@ -832,8 +832,8 @@ public class DefaultCodegen implements CodegenConfig {
/**
* Return the name with escaped characters.
*
* @param name the name to be escaped
* @param charactersToAllow characters that are not escaped
* @param name the name to be escaped
* @param charactersToAllow characters that are not escaped
* @param appdendixToReplacement String to append to replaced characters.
* @return the escaped word
* <p>
@ -841,15 +841,15 @@ public class DefaultCodegen implements CodegenConfig {
*/
public String escapeSpecialCharacters(String name, List<String> charactersToAllow, String appdendixToReplacement) {
String result = (String) ((CharSequence) name).chars().mapToObj(c -> {
String character = "" + (char) c;
if (charactersToAllow != null && charactersToAllow.contains(character)) {
return character;
} else if (specialCharReplacements.containsKey(character)) {
return specialCharReplacements.get(character) + (appdendixToReplacement != null ? appdendixToReplacement: "");
} else {
return character;
}
}).reduce( (c1, c2) -> "" + c1 + c2).orElse(null);
String character = "" + (char) c;
if (charactersToAllow != null && charactersToAllow.contains(character)) {
return character;
} else if (specialCharReplacements.containsKey(character)) {
return specialCharReplacements.get(character) + (appdendixToReplacement != null ? appdendixToReplacement : "");
} else {
return character;
}
}).reduce((c1, c2) -> "" + c1 + c2).orElse(null);
if (result != null) return result;
throw new RuntimeException("Word '" + name + "' could not be escaped.");
@ -1683,13 +1683,13 @@ public class DefaultCodegen implements CodegenConfig {
}
private CodegenDiscriminator createDiscriminator(String schemaName, Schema schema, Map<String, Schema> allDefinitions) {
if(schema.getDiscriminator() == null) {
if (schema.getDiscriminator() == null) {
return null;
}
CodegenDiscriminator discriminator = new CodegenDiscriminator();
discriminator.setPropertyName(schema.getDiscriminator().getPropertyName());
discriminator.setMapping(schema.getDiscriminator().getMapping());
if(schema.getDiscriminator().getMapping() != null && !schema.getDiscriminator().getMapping().isEmpty()) {
if (schema.getDiscriminator().getMapping() != null && !schema.getDiscriminator().getMapping().isEmpty()) {
for (Entry<String, String> e : schema.getDiscriminator().getMapping().entrySet()) {
String name = ModelUtils.getSimpleRef(e.getValue());
discriminator.getMappedModels().add(new MappedModel(e.getKey(), name));
@ -1698,9 +1698,9 @@ public class DefaultCodegen implements CodegenConfig {
allDefinitions.forEach((childName, child) -> {
if (child instanceof ComposedSchema && ((ComposedSchema) child).getAllOf() != null) {
Set<String> parentSchemas = ((ComposedSchema) child).getAllOf().stream()
.filter(s -> s.get$ref() != null)
.map(s -> ModelUtils.getSimpleRef(s.get$ref()))
.collect(Collectors.toSet());
.filter(s -> s.get$ref() != null)
.map(s -> ModelUtils.getSimpleRef(s.get$ref()))
.collect(Collectors.toSet());
if (parentSchemas.contains(schemaName)) {
discriminator.getMappedModels().add(new MappedModel(childName, childName));
}
@ -1786,6 +1786,13 @@ public class DefaultCodegen implements CodegenConfig {
if (p.getReadOnly() != null) {
property.isReadOnly = p.getReadOnly();
}
if (p.getWriteOnly() != null) {
property.isWriteOnly = p.getWriteOnly();
}
if (p.getNullable() != null) {
property.isNullable = p.getNullable();
}
if (p.getXml() != null) {
if (p.getXml().getAttribute() != null) {
property.isXmlAttribute = p.getXml().getAttribute();
@ -2506,7 +2513,7 @@ public class DefaultCodegen implements CodegenConfig {
/**
* Convert OAS Response object to Codegen Response object
*
* @param openAPI a OAS object representing the spec
* @param openAPI a OAS object representing the spec
* @param responseCode HTTP response code
* @param response OAS Response object
* @return Codegen Response object
@ -3495,7 +3502,7 @@ public class DefaultCodegen implements CodegenConfig {
*/
public String apiDocFilename(String templateName, String tag) {
String docExtension = getDocExtension();
String suffix = docExtension != null ? docExtension: apiDocTemplateFiles().get(templateName);
String suffix = docExtension != null ? docExtension : apiDocTemplateFiles().get(templateName);
return apiDocFileFolder() + File.separator + toApiDocFilename(tag) + suffix;
}
@ -4430,7 +4437,7 @@ public class DefaultCodegen implements CodegenConfig {
}
if (StringUtils.isEmpty(bodyParameterName)) {
if(StringUtils.isEmpty(mostInnerItem.complexType)) {
if (StringUtils.isEmpty(mostInnerItem.complexType)) {
codegenParameter.baseName = "request_body";
} else {
codegenParameter.baseName = mostInnerItem.complexType;
@ -4602,7 +4609,7 @@ public class DefaultCodegen implements CodegenConfig {
return Collections.emptyList();
}
List<CodegenServer> codegenServers = new LinkedList<>();
for (Server server: servers) {
for (Server server : servers) {
CodegenServer cs = new CodegenServer();
cs.description = escapeText(server.getDescription());
cs.url = server.getUrl();
@ -4618,7 +4625,7 @@ public class DefaultCodegen implements CodegenConfig {
return Collections.emptyList();
}
List<CodegenServerVariable> codegenServerVariables = new LinkedList<>();
for (Entry<String, ServerVariable> variableEntry: variables.entrySet()) {
for (Entry<String, ServerVariable> variableEntry : variables.entrySet()) {
CodegenServerVariable codegenServerVariable = new CodegenServerVariable();
ServerVariable variable = variableEntry.getValue();
codegenServerVariable.defaultValue = variable.getDefault();

View File

@ -23,6 +23,7 @@ import org.openapitools.codegen.languages.RubyClientCodegen;
import io.swagger.parser.OpenAPIParser;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.parser.core.models.ParseOptions;
import org.apache.commons.io.FileUtils;
@ -169,4 +170,61 @@ public class RubyClientCodegenTest {
Assert.assertEquals(bp.example, "OnlinePetstore::Pet.new");
}
@Test(description = "test nullable for properties")
public void nullableTest() {
final OpenAPI openAPI = new OpenAPIParser().readLocation("src/test/resources/3_0/petstore_oas3_test.yaml", null, new ParseOptions()).getOpenAPI();
final RubyClientCodegen codegen = new RubyClientCodegen();
codegen.setModuleName("OnlinePetstore");
final String path = "/pet";
final Schema schema = openAPI.getComponents().getSchemas().get("NullablePet");
CodegenModel nullablePet = codegen.fromModel("NullablePet", schema, openAPI.getComponents().getSchemas());
CodegenProperty cp0 = nullablePet.getVars().get(0);
Assert.assertTrue(cp0.isNullable);
CodegenProperty cp1 = nullablePet.getVars().get(1);
Assert.assertFalse(cp1.isNullable);
CodegenProperty cp2 = nullablePet.getVars().get(2);
Assert.assertTrue(cp2.isNullable);
CodegenProperty cp3 = nullablePet.getVars().get(3);
Assert.assertTrue(cp3.isNullable);
CodegenProperty cp4 = nullablePet.getVars().get(4);
Assert.assertFalse(cp4.isNullable);
CodegenProperty cp5 = nullablePet.getVars().get(5);
Assert.assertTrue(cp5.isNullable);
}
@Test(description = "test properties without nullable")
public void propertiesWithoutNullableTest() {
final OpenAPI openAPI = new OpenAPIParser().readLocation("src/test/resources/3_0/petstore_oas3_test.yaml", null, new ParseOptions()).getOpenAPI();
final RubyClientCodegen codegen = new RubyClientCodegen();
codegen.setModuleName("OnlinePetstore");
final String path = "/pet";
final Schema schema = openAPI.getComponents().getSchemas().get("Pet");
CodegenModel nullablePet = codegen.fromModel("Pet", schema, openAPI.getComponents().getSchemas());
CodegenProperty cp0 = nullablePet.getVars().get(0);
Assert.assertFalse(cp0.isNullable);
CodegenProperty cp1 = nullablePet.getVars().get(1);
Assert.assertFalse(cp1.isNullable);
CodegenProperty cp2 = nullablePet.getVars().get(2);
Assert.assertFalse(cp2.isNullable);
CodegenProperty cp3 = nullablePet.getVars().get(3);
Assert.assertFalse(cp3.isNullable);
CodegenProperty cp4 = nullablePet.getVars().get(4);
Assert.assertFalse(cp4.isNullable);
CodegenProperty cp5 = nullablePet.getVars().get(5);
Assert.assertFalse(cp5.isNullable);
}
}

View File

@ -0,0 +1,741 @@
openapi: 3.0.0
servers:
- url: 'http://petstore.swagger.io/v2'
info:
description: >-
A test spec based on the Petstore spec with OAS3 related test cases such as nullable
version: 1.0.0
title: OpenAPI Petstore
license:
name: Apache-2.0
url: 'http://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:
'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:
'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:
- 'write:pets'
- '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:
- 'write:pets'
- '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 generated 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
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
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
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
- name: password
in: query
description: The password for login in clear text
required: true
schema:
type: string
responses:
'200':
description: successful operation
headers:
X-Rate-Limit:
description: calls per hour allowed by the user
schema:
type: integer
format: int32
X-Expires-After:
description: date in UTC when toekn 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
'/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
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
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
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
NullablePet:
title: a Pet
description: A pet for sale in the pet store
type: object
required:
- name
- photoUrls
properties:
id:
nullable: true
type: integer
format: int64
category:
$ref: '#/components/schemas/Category'
name:
nullable: true
type: string
example: doggie
photoUrls:
nullable: true
type: array
xml:
name: photoUrl
wrapped: true
items:
type: string
tags:
type: array
xml:
name: tag
wrapped: true
items:
$ref: '#/components/schemas/Tag'
status:
nullable: true
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