Skip to content

Commit

Permalink
Merge pull request payara#5331 from aubi/FISH-1204
Browse files Browse the repository at this point in the history
FISH-1204 Missing example values in OpenApi components schema definition using nested objects
  • Loading branch information
aubi authored and JamesHillyard committed Oct 8, 2021
1 parent e1cec75 commit e0c842c
Show file tree
Hide file tree
Showing 7 changed files with 535 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) [2018-2020] Payara Foundation and/or its affiliates. All rights reserved.
* Copyright (c) [2021] Payara Foundation and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
Expand Down Expand Up @@ -43,30 +43,25 @@
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;

import fish.payara.microprofile.openapi.api.visitor.ApiContext;
import fish.payara.microprofile.openapi.impl.model.ExtensibleImpl;
import fish.payara.microprofile.openapi.impl.model.ExternalDocumentationImpl;
import fish.payara.microprofile.openapi.impl.model.util.ModelUtils;
import fish.payara.microprofile.openapi.impl.rest.app.provider.ObjectMapperFactory;

import static fish.payara.microprofile.openapi.impl.model.util.ModelUtils.applyReference;
import static fish.payara.microprofile.openapi.impl.model.util.ModelUtils.createList;
import static fish.payara.microprofile.openapi.impl.model.util.ModelUtils.createMap;
import static fish.payara.microprofile.openapi.impl.model.util.ModelUtils.extractAnnotations;
import static fish.payara.microprofile.openapi.impl.model.util.ModelUtils.mergeImmutableList;
import static fish.payara.microprofile.openapi.impl.model.util.ModelUtils.mergeProperty;
import static fish.payara.microprofile.openapi.impl.model.util.ModelUtils.readOnlyView;
import fish.payara.microprofile.openapi.impl.rest.app.provider.ObjectMapperFactory;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import static java.util.logging.Level.WARNING;

import java.util.logging.Logger;

import org.eclipse.microprofile.openapi.models.ExternalDocumentation;
import org.eclipse.microprofile.openapi.models.media.Discriminator;
import org.eclipse.microprofile.openapi.models.media.Schema;
Expand Down Expand Up @@ -133,20 +128,25 @@ public static SchemaImpl valueOf(String content) throws JsonMappingException, Js
.readValue(content, SchemaImpl.class);
}

@SuppressWarnings("unchecked")
public static SchemaImpl createInstance(AnnotationModel annotation, ApiContext context) {
SchemaImpl from = new SchemaImpl();

if (annotation == null) {
return from;
}

// Solve the required attribute before "ref" as it is the only one which doesn't conflict with it.
final Boolean isRequired = annotation.getValue("required", Boolean.class);
if (isRequired != null) {
from.isRequired = isRequired;
}

String ref = annotation.getValue("ref", String.class);
if (ref != null && !ref.isEmpty()) {
from.setRef(ref);
return from;
}

EnumModel typeEnum = annotation.getValue("type", EnumModel.class);
if (typeEnum != null) {
from.setType(SchemaType.valueOf(typeEnum.getValue()));
Expand Down Expand Up @@ -184,11 +184,6 @@ public static SchemaImpl createInstance(AnnotationModel annotation, ApiContext c
from.setMinProperties(annotation.getValue("minProperties", Integer.class));
from.setRequired(annotation.getValue("requiredProperties", List.class));

final Boolean isRequired = annotation.getValue("required", Boolean.class);
if (isRequired != null) {
from.isRequired = isRequired;
}

extractAnnotations(annotation, context, "properties", "name", SchemaImpl::createInstance, from::addProperty);
for (Entry<String, Schema> property : from.getProperties().entrySet()) {
final SchemaImpl propertySchema = (SchemaImpl) property.getValue();
Expand Down Expand Up @@ -925,7 +920,7 @@ private static void setImplementation(SchemaImpl schema, String implementationCl
} else {
schemaName = ModelUtils.getSimpleName(implementationClass);
}
// Get the schema reference, and copy it's values over to the new schema model
// Get the schema reference, and copy it's values over to the new schema model if they are missing
Schema copyFrom = context.getApi().getComponents().getSchemas().get(schemaName);
if (copyFrom == null) {
// If the class hasn't been parsed
Expand All @@ -934,9 +929,9 @@ private static void setImplementation(SchemaImpl schema, String implementationCl
}
if (schema.getType() == SchemaType.ARRAY) {
schema.setItems(new SchemaImpl());
ModelUtils.merge(copyFrom, schema.getItems(), true);
ModelUtils.merge(copyFrom, schema.getItems(), false);
} else {
ModelUtils.merge(copyFrom, schema, true);
ModelUtils.merge(copyFrom, schema, false);
}
schema.setRef(null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@
*/
package fish.payara.microprofile.openapi.impl.processor;

import static fish.payara.microprofile.openapi.impl.model.util.ModelUtils.isVoid;

import fish.payara.microprofile.openapi.api.processor.OASProcessor;
import fish.payara.microprofile.openapi.api.visitor.ApiContext;
import fish.payara.microprofile.openapi.api.visitor.ApiVisitor;
Expand All @@ -63,21 +61,20 @@
import fish.payara.microprofile.openapi.impl.model.servers.ServerImpl;
import fish.payara.microprofile.openapi.impl.model.tags.TagImpl;
import fish.payara.microprofile.openapi.impl.model.util.ModelUtils;
import static fish.payara.microprofile.openapi.impl.model.util.ModelUtils.isVoid;
import fish.payara.microprofile.openapi.impl.visitor.OpenApiWalker;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.Map.Entry;

import java.util.Set;
import java.util.logging.Level;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.SEVERE;
import static java.util.logging.Level.WARNING;

import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import jakarta.ws.rs.DefaultValue;
Expand All @@ -88,17 +85,17 @@
import org.eclipse.microprofile.openapi.models.Components;
import org.eclipse.microprofile.openapi.models.ExternalDocumentation;
import org.eclipse.microprofile.openapi.models.OpenAPI;
import org.eclipse.microprofile.openapi.models.Operation;
import org.eclipse.microprofile.openapi.models.PathItem;
import org.eclipse.microprofile.openapi.models.Reference;
import org.eclipse.microprofile.openapi.models.callbacks.Callback;
import org.eclipse.microprofile.openapi.models.media.MediaType;
import org.eclipse.microprofile.openapi.models.media.Schema;
import org.eclipse.microprofile.openapi.models.media.Schema.SchemaType;
import org.eclipse.microprofile.openapi.models.parameters.Parameter;
import org.eclipse.microprofile.openapi.models.parameters.Parameter.In;
import org.eclipse.microprofile.openapi.models.parameters.Parameter.Style;
import org.eclipse.microprofile.openapi.models.parameters.RequestBody;
import org.eclipse.microprofile.openapi.models.Operation;
import org.eclipse.microprofile.openapi.models.media.Schema.SchemaType;
import org.eclipse.microprofile.openapi.models.responses.APIResponse;
import org.eclipse.microprofile.openapi.models.responses.APIResponses;
import org.eclipse.microprofile.openapi.models.security.SecurityRequirement;
Expand All @@ -113,9 +110,9 @@
import org.glassfish.hk2.classmodel.reflect.FieldModel;
import org.glassfish.hk2.classmodel.reflect.MethodModel;
import org.glassfish.hk2.classmodel.reflect.ParameterizedInterfaceModel;
import org.glassfish.hk2.classmodel.reflect.ParameterizedType;
import org.glassfish.hk2.classmodel.reflect.Type;
import org.glassfish.hk2.classmodel.reflect.Types;
import org.glassfish.hk2.classmodel.reflect.ParameterizedType;

/**
* A processor to parse the application for annotations, to add to the OpenAPI
Expand Down Expand Up @@ -635,7 +632,7 @@ private void visitSchemaField(AnnotationModel schemaAnnotation, FieldModel field
visitSchemaFieldOrMethod(schemaAnnotation, field, declaringType, typeName, context);
}

public void visitSchemaFieldOrMethod(AnnotationModel schemaAnnotation, AnnotatedElement fieldOrMethod,
private void visitSchemaFieldOrMethod(AnnotationModel schemaAnnotation, AnnotatedElement fieldOrMethod,
ExtensibleType<?> declaringType, String typeName, ApiContext context) {
assert (fieldOrMethod instanceof FieldModel) || (fieldOrMethod instanceof MethodModel);

Expand Down Expand Up @@ -1252,6 +1249,14 @@ private Schema createSchema(
schema.setItems(null);
}
}
if (type instanceof AnnotatedElement) {
AnnotatedElement element = (AnnotatedElement) type;
final AnnotationModel schemaAnnotation = element
.getAnnotation(org.eclipse.microprofile.openapi.annotations.media.Schema.class.getName());
if (schemaAnnotation != null) {
SchemaImpl.merge(SchemaImpl.createInstance(schemaAnnotation, context), schema, false, context);
}
}

return schema;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) [2021] Payara Foundation and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://github.com/payara/Payara/blob/master/LICENSE.txt
* See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at glassfish/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* The Payara Foundation designates this particular file as subject to the "Classpath"
* exception as provided by the Payara Foundation in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package fish.payara.microprofile.openapi.test.app.application;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import fish.payara.microprofile.openapi.resource.rule.ApplicationProcessedDocument;
import fish.payara.microprofile.openapi.test.app.OpenApiApplicationTest;
import fish.payara.microprofile.openapi.test.app.TestApplication;
import fish.payara.microprofile.openapi.test.app.application.schema.Schema1Depending;
import fish.payara.microprofile.openapi.test.app.application.schema.Schema2Simple;
import fish.payara.microprofile.openapi.test.app.application.schema.Schema2Simple1;
import static fish.payara.microprofile.openapi.test.util.JsonUtils.path;
import static fish.payara.microprofile.openapi.test.util.JsonUtils.toJson;
import java.util.Iterator;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import org.junit.Before;
import org.junit.Test;

/**
* Test presence of required attribute in the generated OpenApi.
*/
@Path("/serversDependant")
public class DependantClassesTest extends OpenApiApplicationTest {

// add multiple classes to be processed, simulate component scan
@Before
@Override
public void createDocument() {
document = ApplicationProcessedDocument.createDocument(null, getClass(), TestApplication.class, Schema1Depending.class, Schema2Simple.class, Schema2Simple1.class);
jsonDocument = toJson(document);
}

@POST
@Produces(MediaType.APPLICATION_JSON)
@APIResponses({
@APIResponse(responseCode = "200", description = "Successful, returning ok.")})
public Schema1Depending save(@RequestBody(description = "The request body with connected data", required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = Schema1Depending.class)
)) Schema1Depending schema1Depending) {
return new Schema1Depending();
}

/**
* Test if the additional field description is copied to the dependant
* schema. E.g. Schema2Simple -> Schema1Depending
*/
@Test
public void dependantSchemaIsFullyPopulated() {
ObjectNode root = getOpenAPIJson();
// verify schema
JsonNode subpropertyId = path(root, "components.schemas.Schema1Depending.properties.schema2Simple.properties.id");
assertEquals("ID", subpropertyId.findValue("description").asText());
assertEquals("1", subpropertyId.findValue("example").asText());
// verify operation's data
// FIXME: this test crashes as for some reason, schema2Simple is refereneced, not copied.
JsonNode operationSubpropertyId = path(root, "paths./test/serversDependant.post.requestBody.content.application/json.schema.properties.schema2Simple.properties.id");
assertEquals("ID", operationSubpropertyId.findValue("description").asText());
assertEquals("1", operationSubpropertyId.findValue("example").asText());
}

@Test
public void dependantSchemaHasTwoRequiredFields() {
// verify, that both attributes in Schema1Depending (implementation and ref) are required
JsonNode requiredItems = path(getOpenAPIJson(), "components.schemas.Schema1Depending.required");
assertNotNull(requiredItems);
Iterator<JsonNode> requiredElements = requiredItems.elements();
// verify one result
assertTrue(requiredElements.hasNext());
// verify the name of the required field
assertEquals("schema2Simple", requiredElements.next().asText());
// one more result
assertTrue(requiredElements.hasNext());
// verify the name of the required field
assertEquals("schema2SimpleRef", requiredElements.next().asText());
// no more than two results
assertFalse(requiredElements.hasNext());
}
}
Loading

0 comments on commit e0c842c

Please sign in to comment.