[core] Add x-is-free-form vendor extension (#6849)

This adds an x-is-free-form vendor extension to allow users to skip our
"free-form" logic which would previously prevent object schemas with no
properties to be considered "free-form". The previous behavior was due
in part to Swagger Parser not exposing `additionalProperties: false` to
us (which should be similar behavior to this extension).

A free-form object is considered a dynamic object with any number of
properties/types. DefaultGenerator does not allow for generation of
models considered free-form. However, a base type with no properties and
no additional properties is allowed by OpenAPI Specification and is
meaningful in many languages (e.g. "marker interfaces" or abstract
closed types).
This commit is contained in:
Jim Schubert 2020-08-24 19:00:47 -04:00 committed by GitHub
parent 54a6c791f7
commit a97feaf533
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 128 additions and 9 deletions

View File

@ -65,6 +65,8 @@ public class ModelUtils {
// A vendor extension to track the value of the 'disallowAdditionalPropertiesIfNotPresent' CLI // A vendor extension to track the value of the 'disallowAdditionalPropertiesIfNotPresent' CLI
private static final String disallowAdditionalPropertiesIfNotPresent = "x-disallow-additional-properties-if-not-present"; private static final String disallowAdditionalPropertiesIfNotPresent = "x-disallow-additional-properties-if-not-present";
private static final String freeFormExplicit = "x-is-free-form";
private static ObjectMapper JSON_MAPPER, YAML_MAPPER; private static ObjectMapper JSON_MAPPER, YAML_MAPPER;
static { static {
@ -672,25 +674,23 @@ public class ModelUtils {
} }
/** /**
* Check to see if the schema is a model with at least one property. * Check to see if the schema is a model
* *
* @param schema potentially containing a '$ref' * @param schema potentially containing a '$ref'
* @return true if it's a model with at least one properties * @return true if it's a model with at least one properties
*/ */
public static boolean isModel(Schema schema) { public static boolean isModel(Schema schema) {
if (schema == null) { if (schema == null) {
// TODO: Is this message necessary? A null schema is not a model, so the result is correct.
once(LOGGER).error("Schema cannot be null in isModel check");
return false; return false;
} }
// has at least one property // has properties
if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { if (null != schema.getProperties()) {
return true; return true;
} }
// composed schema is a model // composed schema is a model, consider very simple ObjectSchema a model
return schema instanceof ComposedSchema; return schema instanceof ComposedSchema || schema instanceof ObjectSchema;
} }
/** /**
@ -745,6 +745,16 @@ public class ModelUtils {
// no properties // no properties
if ((schema.getProperties() == null || schema.getProperties().isEmpty())) { if ((schema.getProperties() == null || schema.getProperties().isEmpty())) {
Schema addlProps = getAdditionalProperties(openAPI, schema); Schema addlProps = getAdditionalProperties(openAPI, schema);
if (schema.getExtensions() != null && schema.getExtensions().containsKey(freeFormExplicit)) {
// User has hard-coded vendor extension to handle free-form evaluation.
boolean isFreeFormExplicit = Boolean.parseBoolean(String.valueOf(schema.getExtensions().get(freeFormExplicit)));
if (!isFreeFormExplicit && addlProps != null && addlProps.getProperties() != null && !addlProps.getProperties().isEmpty()) {
once(LOGGER).error(String.format(Locale.ROOT, "Potentially confusing usage of %s within model which defines additional properties", freeFormExplicit));
}
return isFreeFormExplicit;
}
// additionalProperties not defined // additionalProperties not defined
if (addlProps == null) { if (addlProps == null) {
return true; return true;

View File

@ -927,6 +927,35 @@ public class JavaClientCodegenTest {
); );
} }
@Test
public void testAllowModelWithNoProperties() throws Exception {
File output = Files.createTempDirectory("test").toFile();
final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("java")
.setLibrary(JavaClientCodegen.OKHTTP_GSON)
.setInputSpec("src/test/resources/2_0/emptyBaseModel.yaml")
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));
final ClientOptInput clientOptInput = configurator.toClientOptInput();
DefaultGenerator generator = new DefaultGenerator();
List<File> files = generator.opts(clientOptInput).generate();
Assert.assertEquals(files.size(), 47);
TestUtils.ensureContainsFile(files, output, "src/main/java/org/openapitools/client/model/RealCommand.java");
TestUtils.ensureContainsFile(files, output, "src/main/java/org/openapitools/client/model/Command.java");
validateJavaSourceFiles(files);
TestUtils.assertFileContains(Paths.get(output + "/src/main/java/org/openapitools/client/model/RealCommand.java"),
"class RealCommand extends Command");
TestUtils.assertFileContains(Paths.get(output + "/src/main/java/org/openapitools/client/model/Command.java"),
"class Command");
output.deleteOnExit();
}
/** /**
* See https://github.com/OpenAPITools/openapi-generator/issues/6715 * See https://github.com/OpenAPITools/openapi-generator/issues/6715
*/ */

View File

@ -128,6 +128,15 @@ public class ModelUtilsTest {
Assert.assertEquals(unusedSchemas.size(), 0); Assert.assertEquals(unusedSchemas.size(), 0);
} }
@Test
public void testIsModelAllowsEmptyBaseModel() {
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/2_0/emptyBaseModel.yaml");
Schema commandSchema = ModelUtils.getSchema(openAPI, "Command");
Assert.assertTrue(ModelUtils.isModel(commandSchema));
Assert.assertFalse(ModelUtils.isFreeFormObject(openAPI, commandSchema));
}
@Test @Test
public void testReferencedSchema() { public void testReferencedSchema() {
Schema otherObj = new ObjectSchema().addProperties("sprop", new StringSchema()).addProperties("iprop", new IntegerSchema()); Schema otherObj = new ObjectSchema().addProperties("sprop", new StringSchema()).addProperties("iprop", new IntegerSchema());

View File

@ -0,0 +1,71 @@
swagger: "2.0"
info:
title: Test Command model generation
description: Test Command model generation
version: 1.0.0
host: localhost:8080
schemes:
- https
definitions:
Command:
title: Command
description: The base object for all command objects.
type: object
# Explicitly avoid treating as a "free-form" or dynamic object, resulting in classical languages as a class with no properties.
x-is-free-form: false
RealCommand:
title: RealCommand
description: The real command.
allOf:
- $ref: '#/definitions/Command'
ApiError:
description: The base object for API errors.
type: object
required:
- code
- message
properties:
code:
description: The error code. Usually, it is the HTTP error code.
type: string
readOnly: true
message:
description: The error message.
type: string
readOnly: true
title: ApiError
parameters:
b_real_command:
name: real_command
in: body
description: A payload for executing a real command.
required: true
schema:
$ref: '#/definitions/RealCommand'
paths:
/execute:
post:
produces: []
operationId: executeRealCommand
parameters:
- name: real_command
in: body
description: A payload for executing a real command.
required: true
schema:
$ref: '#/definitions/RealCommand'
responses:
'204':
description: Successful request. No content returned.
'400':
description: Bad request.
schema:
$ref: '#/definitions/ApiError'
'404':
description: Not found.
schema:
$ref: '#/definitions/ApiError'
default:
description: Unknown error.
schema:
$ref: '#/definitions/ApiError'