Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.swagger.v3.core.jackson.mixin;

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.models.Operation;

import java.util.Map;

/**
* Mixin applied to {@link io.swagger.v3.oas.models.PathItem} for OpenAPI 3.0 and 3.1 mappers.
*
* <p>The {@code query} method is a fixed field of the Path Item Object only as of OpenAPI 3.2,
* so it is ignored here to keep 3.0 and 3.1 documents conformant.
*/
public abstract class PathItemMixin {

@JsonAnyGetter
public abstract Map<String, Object> getExtensions();

@JsonAnySetter
public abstract void addExtension(String name, Object value);

@JsonIgnore
public abstract Operation getQuery();
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import io.swagger.v3.core.jackson.mixin.OpenAPI31Mixin;
import io.swagger.v3.core.jackson.mixin.OpenAPIMixin;
import io.swagger.v3.core.jackson.mixin.OperationMixin;
import io.swagger.v3.core.jackson.mixin.PathItemMixin;
import io.swagger.v3.core.jackson.mixin.Schema31Mixin;
import io.swagger.v3.core.jackson.mixin.SchemaConverterMixin;
import io.swagger.v3.core.jackson.mixin.SchemaMixin;
Expand Down Expand Up @@ -186,7 +187,7 @@ public JsonSerializer<?> modifySerializer(
sourceMixins.put(OAuthFlow.class, ExtensionsMixin.class);
sourceMixins.put(OAuthFlows.class, ExtensionsMixin.class);
sourceMixins.put(Operation.class, OperationMixin.class);
sourceMixins.put(PathItem.class, ExtensionsMixin.class);
sourceMixins.put(PathItem.class, PathItemMixin.class);
sourceMixins.put(Paths.class, ExtensionsMixin.class);
sourceMixins.put(Scopes.class, ExtensionsMixin.class);
sourceMixins.put(Server.class, ExtensionsMixin.class);
Expand Down Expand Up @@ -261,7 +262,7 @@ public static ObjectMapper createJsonConverter() {
sourceMixins.put(OpenAPI.class, OpenAPIMixin.class);
sourceMixins.put(Operation.class, OperationMixin.class);
sourceMixins.put(Parameter.class, ExtensionsMixin.class);
sourceMixins.put(PathItem.class, ExtensionsMixin.class);
sourceMixins.put(PathItem.class, PathItemMixin.class);
sourceMixins.put(Paths.class, ExtensionsMixin.class);
sourceMixins.put(RequestBody.class, ExtensionsMixin.class);
sourceMixins.put(Scopes.class, ExtensionsMixin.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import io.swagger.v3.core.matchers.SerializationMatchers;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.core.util.Json31;
import io.swagger.v3.core.util.ObjectMapperFactory;
import io.swagger.v3.core.util.ResourceUtils;
import io.swagger.v3.core.util.Yaml;
Expand All @@ -23,6 +24,9 @@

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;

public class JsonSerializationTest {

Expand All @@ -42,6 +46,46 @@ public void testSerializeASpecWithPathReferences() throws Exception {
assertEquals(path, expectedPath);
}

@Test
public void testQueryHttpMethodIsNotSerializedBeforeOpenAPI32() throws Exception {

Operation queryOperation = new Operation()
.operationId("searchPets")
.responses(new ApiResponses()
.addApiResponse("200", new ApiResponse().description("ok")));

PathItem pathItem = new PathItem()
.get(new Operation().operationId("listPets"))
.query(queryOperation);
OpenAPI openAPI = new OpenAPI().path("/pets", pathItem);

// the model holds the operation ...
assertNotNull(openAPI.getPaths().get("/pets").getQuery());

// ... but "query" is a Path Item fixed field only as of OpenAPI 3.2, so neither the
// 3.0 nor the 3.1 mapper may emit it, while the other methods serialize as usual
for (String json : new String[]{
Json.mapper().writeValueAsString(openAPI),
Json31.mapper().writeValueAsString(openAPI)}) {
assertFalse(json.contains("\"query\""));
assertTrue(json.contains("\"listPets\""));
assertFalse(json.contains("\"searchPets\""));
}
}

@Test
public void testQueryHttpMethodIsNotDeserializedBeforeOpenAPI32() throws Exception {

String json = "{\"openapi\":\"3.0.1\",\"paths\":{\"/pets\":{"
+ "\"get\":{\"operationId\":\"listPets\"},"
+ "\"query\":{\"operationId\":\"searchPets\"}}}}";

PathItem pathItem = Json.mapper().readValue(json, OpenAPI.class).getPaths().get("/pets");

assertNotNull(pathItem.getGet());
assertNull(pathItem.getQuery());
}

@Test
public void testExtension() throws Exception {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ public class Reader implements OpenApiReader {
private static final String TRACE_METHOD = "trace";
private static final String HEAD_METHOD = "head";
private static final String OPTIONS_METHOD = "options";
private static final String QUERY_METHOD = "query";

public Reader() {
this(new OpenAPI(), new Paths(), new LinkedHashSet<>(), new Components());
Expand Down Expand Up @@ -1397,6 +1398,9 @@ private void setPathItemOperation(PathItem pathItemObject, String method, Operat
case OPTIONS_METHOD:
pathItemObject.options(operation);
break;
case QUERY_METHOD:
pathItemObject.query(operation);
break;
default:
// Do nothing here
break;
Expand Down Expand Up @@ -1565,6 +1569,9 @@ private Set<String> extractOperationIdFromPathItem(PathItem path) {
if (path.getPatch() != null && StringUtils.isNotBlank(path.getPatch().getOperationId())) {
ids.add(path.getPatch().getOperationId());
}
if (path.getQuery() != null && StringUtils.isNotBlank(path.getQuery().getOperationId())) {
ids.add(path.getQuery().getOperationId());
}
return ids;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package io.swagger.v3.jaxrs2;

import io.swagger.v3.core.util.Json;
import io.swagger.v3.core.util.Json31;
import io.swagger.v3.core.util.Yaml;
import io.swagger.v3.core.util.Yaml31;
import io.swagger.v3.jaxrs2.resources.QueryMethodResource;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.PathItem;
import org.testng.annotations.Test;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;

/**
* Verifies that the reader maps a custom {@code @HttpMethod("QUERY")} annotation
* (OpenAPI 3.2 query method) onto {@link PathItem#getQuery()}.
*/
public class QueryMethodTest {

@Test(description = "read a resource declaring the OpenAPI 3.2 QUERY method")
public void testQueryHttpMethod() {
Reader reader = new Reader(new OpenAPI());
OpenAPI openAPI = reader.read(QueryMethodResource.class);

PathItem pathItem = openAPI.getPaths().get("/pets/search");
assertNotNull(pathItem);

assertNotNull(pathItem.getQuery());
assertEquals(pathItem.getQuery().getOperationId(), "searchPets");

// the operation must land on query only, not leak onto other methods
assertNull(pathItem.getGet());
assertNull(pathItem.getPost());
}

@Test(description = "a QUERY operation must not reach OpenAPI 3.0 or 3.1 output")
public void testQueryHttpMethodIsNotSerializedBeforeOpenAPI32() throws Exception {
Reader reader = new Reader(new OpenAPI());
OpenAPI openAPI = reader.read(QueryMethodResource.class);

// "query" is a Path Item fixed field only as of OpenAPI 3.2, so the operation the
// reader resolved stays in the model but is gated out of pre-3.2 documents
for (String serialized : new String[]{
Yaml.mapper().writeValueAsString(openAPI),
Yaml31.mapper().writeValueAsString(openAPI),
Json.mapper().writeValueAsString(openAPI),
Json31.mapper().writeValueAsString(openAPI)}) {
assertFalse(serialized.contains("query"));
assertFalse(serialized.contains("searchPets"));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.swagger.v3.jaxrs2.resources;

import javax.ws.rs.HttpMethod;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Stands in for the {@code @QUERY} binding an application declares itself, since JAX-RS /
* Jakarta REST does not ship one for the OpenAPI 3.2 {@code QUERY} method.
*
* <p>It is meta-annotated with {@link HttpMethod} in the same way {@code javax.ws.rs.PATCH} is,
* which is all the reader needs: {@code ReaderUtils.getHttpMethodFromCustomAnnotations} resolves
* any such annotation to its lower-cased method name, which is then mapped onto
* {@link io.swagger.v3.oas.models.PathItem#getQuery()}.
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@HttpMethod("QUERY")
public @interface QUERY {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.swagger.v3.jaxrs2.resources;

import io.swagger.v3.oas.annotations.Operation;

import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;

@Path("/pets")
public class QueryMethodResource {

@QUERY
@Path("/search")
@Produces("application/json")
@Operation(operationId = "searchPets", summary = "Search pets")
public Response searchPets() {
return Response.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*
* @see <a href="https://github.com/OAI/OpenAPI-Specification/blob/3.0.4/versions/3.0.4.md#path-item-object">PathItem (OpenAPI 3.0 specification)</a>
* @see <a href="https://github.com/OAI/OpenAPI-Specification/blob/3.1.1/versions/3.1.1.md#path-item-object">PathItem (OpenAPI 3.1 specification)</a>
* @see <a href="https://github.com/OAI/OpenAPI-Specification/blob/3.2.0/versions/3.2.0.md#path-item-object">PathItem (OpenAPI 3.2 specification)</a>
*/

public class PathItem {
Expand All @@ -27,6 +28,7 @@ public class PathItem {
private Operation head = null;
private Operation patch = null;
private Operation trace = null;
private Operation query = null;
private List<Server> servers = null;
private List<Parameter> parameters = null;
private String $ref = null;
Expand Down Expand Up @@ -222,6 +224,30 @@ public PathItem trace(Operation trace) {
return this;
}

/**
* returns the query property from a PathItem instance.
*
* <p>The {@code query} HTTP method was introduced in OpenAPI 3.2. It is therefore not
* serialized by OpenAPI 3.0 and 3.1 mappers, which would otherwise emit a Path Item
* field that is not valid for those versions.
*
* @return Operation query
* @since OpenAPI 3.2
**/

public Operation getQuery() {
return query;
}

public void setQuery(Operation query) {
this.query = query;
}

public PathItem query(Operation query) {
this.query = query;
return this;
}

public List<Operation> readOperations() {
List<Operation> allOperations = new ArrayList<>();
if (this.get != null) {
Expand All @@ -248,6 +274,9 @@ public List<Operation> readOperations() {
if (this.trace != null) {
allOperations.add(this.trace);
}
if (this.query != null) {
allOperations.add(this.query);
}

return allOperations;
}
Expand Down Expand Up @@ -278,6 +307,9 @@ public void operation(HttpMethod method, Operation operation) {
case DELETE:
this.delete = operation;
break;
case QUERY:
this.query = operation;
break;
default:
}
}
Expand All @@ -290,7 +322,8 @@ public enum HttpMethod {
DELETE,
HEAD,
OPTIONS,
TRACE
TRACE,
QUERY
}

public Map<HttpMethod, Operation> readOperationsMap() {
Expand Down Expand Up @@ -320,6 +353,9 @@ public Map<HttpMethod, Operation> readOperationsMap() {
if (this.trace != null) {
result.put(HttpMethod.TRACE, this.trace);
}
if (this.query != null) {
result.put(HttpMethod.QUERY, this.query);
}

return result;
}
Expand Down Expand Up @@ -468,6 +504,9 @@ public boolean equals(Object o) {
if (trace != null ? !trace.equals(pathItem.trace) : pathItem.trace != null) {
return false;
}
if (query != null ? !query.equals(pathItem.query) : pathItem.query != null) {
return false;
}
if (servers != null ? !servers.equals(pathItem.servers) : pathItem.servers != null) {
return false;
}
Expand All @@ -493,6 +532,7 @@ public int hashCode() {
result = 31 * result + (head != null ? head.hashCode() : 0);
result = 31 * result + (patch != null ? patch.hashCode() : 0);
result = 31 * result + (trace != null ? trace.hashCode() : 0);
result = 31 * result + (query != null ? query.hashCode() : 0);
result = 31 * result + (servers != null ? servers.hashCode() : 0);
result = 31 * result + (parameters != null ? parameters.hashCode() : 0);
result = 31 * result + ($ref != null ? $ref.hashCode() : 0);
Expand All @@ -514,6 +554,7 @@ public String toString() {
sb.append(" head: ").append(toIndentedString(head)).append("\n");
sb.append(" patch: ").append(toIndentedString(patch)).append("\n");
sb.append(" trace: ").append(toIndentedString(trace)).append("\n");
sb.append(" query: ").append(toIndentedString(query)).append("\n");
sb.append(" servers: ").append(toIndentedString(servers)).append("\n");
sb.append(" parameters: ").append(toIndentedString(parameters)).append("\n");
sb.append(" $ref: ").append(toIndentedString($ref)).append("\n");
Expand Down
Loading