Skip to content

Commit

Permalink
Merge pull request #3740 from swagger-api/config-deterministic-sort
Browse files Browse the repository at this point in the history
  • Loading branch information
frantuma authored Dec 3, 2020
2 parents 259eff8 + 2fc7f60 commit 2907229
Show file tree
Hide file tree
Showing 23 changed files with 682 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.links.Link;
import io.swagger.v3.oas.models.links.LinkParameter;
import io.swagger.v3.oas.models.media.DateSchema;import io.swagger.v3.oas.models.media.Encoding;
import io.swagger.v3.oas.models.media.DateSchema;
import io.swagger.v3.oas.models.media.Encoding;
import io.swagger.v3.oas.models.media.EncodingProperty;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -223,6 +224,9 @@ public static boolean isConstructorCompatible(Constructor<?> constructor) {
* excluding <code>Object</code> class. If the field from child class hides the field from superclass,
* the field from superclass won't be added to the result list.
*
* The list is sorted by name to make the output of this method deterministic.
* See https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#getFields--
*
* @param cls is the processing class
* @return list of Fields
*/
Expand All @@ -241,6 +245,10 @@ public static List<Field> getDeclaredFields(Class<?> cls) {
fields.add(field);
}
}

// Make sure the order is deterministic
fields.sort(Comparator.comparing(Field::getName));

return fields;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,4 +367,4 @@ public void testEnumWithNull() throws Exception {
SerializationMatchers.assertEqualsToYaml(model, yaml);

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.swagger.v3.core.util.ReflectionUtils;
import io.swagger.v3.core.util.reflection.resources.Child;
import io.swagger.v3.core.util.reflection.resources.IParent;
import io.swagger.v3.core.util.reflection.resources.ObjectWithManyFields;
import io.swagger.v3.core.util.reflection.resources.Parent;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
Expand All @@ -11,10 +12,13 @@
import org.testng.annotations.Test;

import javax.ws.rs.Path;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import static org.testng.Assert.assertNull;

Expand Down Expand Up @@ -134,6 +138,14 @@ public void getDeclaredFieldsFromInterfaceTest() throws NoSuchMethodException {
Assert.assertEquals(Collections.emptyList(), ReflectionUtils.getDeclaredFields(cls));
}

@Test
public void declaredFieldsShouldBeSorted() {
final Class cls = ObjectWithManyFields.class;
final List<Field> declaredFields = ReflectionUtils.getDeclaredFields(cls);
Assert.assertEquals(4, declaredFields.size());
Assert.assertEquals(Arrays.asList("a", "b", "c", "d"), declaredFields.stream().map(Field::getName).collect(Collectors.toList()));
}

@Test
public void testFindMethodForNullClass() throws Exception {
Method method = ReflectionUtilsTest.class.getMethod("testFindMethodForNullClass", (Class<?>[]) null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.swagger.v3.core.util.reflection.resources;

public class ObjectWithManyFields {

public String a;
public boolean d;
public Integer c;
public Object b;

}
4 changes: 4 additions & 0 deletions modules/swagger-gradle-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Parameter | Description | Required | Default
`resourcePackages`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false|
`resourceClasses`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false|
`prettyPrint`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false|`TRUE`
`sortOutput`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false|`FALSE`
`openApiFile`|openapi file to be merged with resolved specification, equivalent to [config](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties) openAPI|false|
`filterClass`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false|
`readerClass`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false|
Expand Down Expand Up @@ -95,3 +96,6 @@ info:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
```
Since version 2.1.6, `sortOutput` parameter is available, allowing to sort object properties and map keys alphabetically.
Since version 2.1.6, `objectMapperProcessorClass` allows to configure also the ObjectMapper instance used to serialize the resolved OpenAPI
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public enum Format {JSON, YAML, JSONANDYAML};
private LinkedHashSet<String> modelConverterClasses;
private String objectMapperProcessorClass;

private Boolean sortOutput = Boolean.FALSE;

private String contextId;

@Input
Expand Down Expand Up @@ -294,6 +296,17 @@ public void setEncoding(String resourceClasses) {
this.encoding = encoding;
}

@Input
@Optional
public Boolean getSortOutput() {
return sortOutput;
}

public void setSortOutput(Boolean sortOutput) {
this.sortOutput = sortOutput;
}


@TaskAction
public void resolve() throws GradleException {
if (skip) {
Expand Down Expand Up @@ -390,6 +403,9 @@ public void resolve() throws GradleException {
method=swaggerLoaderClass.getDeclaredMethod("setPrettyPrint", Boolean.class);
method.invoke(swaggerLoader, prettyPrint);

method=swaggerLoaderClass.getDeclaredMethod("setSortOutput", Boolean.class);
method.invoke(swaggerLoader, sortOutput);

method=swaggerLoaderClass.getDeclaredMethod("setReadAllResources", Boolean.class);
method.invoke(swaggerLoader, readAllResources);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
package io.swagger.v3.oas.integration;

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.v3.core.converter.ModelConverter;
import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.core.jackson.ModelResolver;
import io.swagger.v3.core.jackson.PathsSerializer;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.core.util.Yaml;
import io.swagger.v3.oas.integration.api.ObjectMapperProcessor;
import io.swagger.v3.oas.integration.api.OpenAPIConfiguration;
import io.swagger.v3.oas.integration.api.OpenApiConfigurationLoader;
import io.swagger.v3.oas.integration.api.OpenApiContext;
import io.swagger.v3.oas.integration.api.OpenApiReader;
import io.swagger.v3.oas.integration.api.OpenApiScanner;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.media.Schema;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.slf4j.Logger;
Expand Down Expand Up @@ -43,6 +56,9 @@ public class GenericOpenApiContext<T extends GenericOpenApiContext> implements O
private ObjectMapperProcessor objectMapperProcessor;
private Set<ModelConverter> modelConverters;

private ObjectMapper outputJsonMapper;
private ObjectMapper outputYamlMapper;

private ConcurrentHashMap<String, Cache> cache = new ConcurrentHashMap<>();

// 0 doesn't cache
Expand Down Expand Up @@ -210,6 +226,52 @@ public final T modelConverters(Set<ModelConverter> modelConverters) {
return (T) this;
}

/**
* @since 2.1.6
*/
public ObjectMapper getOutputJsonMapper() {
return outputJsonMapper;
}

/**
* @since 2.1.6
*/
@Override
public void setOutputJsonMapper(ObjectMapper outputJsonMapper) {
this.outputJsonMapper = outputJsonMapper;
}

/**
* @since 2.1.6
*/
public final T outputJsonMapper(ObjectMapper outputJsonMapper) {
this.outputJsonMapper = outputJsonMapper;
return (T) this;
}

/**
* @since 2.1.6
*/
public ObjectMapper getOutputYamlMapper() {
return outputYamlMapper;
}

/**
* @since 2.1.6
*/
@Override
public void setOutputYamlMapper(ObjectMapper outputYamlMapper) {
this.outputYamlMapper = outputYamlMapper;
}

/**
* @since 2.1.6
*/
public final T outputYamlMapper(ObjectMapper outputYamlMapper) {
this.outputYamlMapper = outputYamlMapper;
return (T) this;
}


protected void register() {
OpenApiContextLocator.getInstance().putOpenApiContext(id, this);
Expand Down Expand Up @@ -363,16 +425,36 @@ public T init() throws OpenApiConfigurationException {
if (modelConverters == null || modelConverters.isEmpty()) {
modelConverters = buildModelConverters(ContextUtils.deepCopy(openApiConfiguration));
}
if (outputJsonMapper == null) {
outputJsonMapper = Json.mapper().copy();
}
if (outputYamlMapper == null) {
outputYamlMapper = Yaml.mapper().copy();
}
if (openApiConfiguration.isSortOutput() != null && openApiConfiguration.isSortOutput()) {
outputJsonMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
outputJsonMapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
outputYamlMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
outputYamlMapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
outputJsonMapper.addMixIn(OpenAPI.class, SortedOpenAPIMixin.class);
outputJsonMapper.addMixIn(Schema.class, SortedSchemaMixin.class);
outputYamlMapper.addMixIn(OpenAPI.class, SortedOpenAPIMixin.class);
outputYamlMapper.addMixIn(Schema.class, SortedSchemaMixin.class);
}
} catch (Exception e) {
LOGGER.error("error initializing context: " + e.getMessage(), e);
throw new OpenApiConfigurationException("error initializing context: " + e.getMessage(), e);
}


try {
if (objectMapperProcessor != null) {
ObjectMapper mapper = IntegrationObjectMapperFactory.createJson();
objectMapperProcessor.processJsonObjectMapper(mapper);
ModelConverters.getInstance().addConverter(new ModelResolver(mapper));

objectMapperProcessor.processOutputJsonObjectMapper(outputJsonMapper);
objectMapperProcessor.processOutputYamlObjectMapper(outputYamlMapper);
}
} catch (Exception e) {
LOGGER.error("error configuring objectMapper: " + e.getMessage(), e);
Expand Down Expand Up @@ -442,6 +524,9 @@ private OpenAPIConfiguration mergeParentConfiguration(OpenAPIConfiguration confi
if (merged.isPrettyPrint() == null) {
merged.setPrettyPrint(parentConfig.isPrettyPrint());
}
if (merged.isSortOutput() == null) {
merged.setSortOutput(parentConfig.isSortOutput());
}
if (merged.isReadAllResources() == null) {
merged.setReadAllResources(parentConfig.isReadAllResources());
}
Expand Down Expand Up @@ -493,4 +578,36 @@ boolean isStale(long cacheTTL) {
}
}

@JsonPropertyOrder(value = {"openapi", "info", "externalDocs", "servers", "security", "tags", "paths", "components"}, alphabetic = true)
static abstract class SortedOpenAPIMixin {

@JsonAnyGetter
@JsonPropertyOrder(alphabetic = true)
public abstract Map<String, Object> getExtensions();

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

@JsonSerialize(using = PathsSerializer.class)
public abstract Paths getPaths();
}

@JsonPropertyOrder(value = {"type", "format"}, alphabetic = true)
static abstract class SortedSchemaMixin {

@JsonAnyGetter
@JsonPropertyOrder(alphabetic = true)
public abstract Map<String, Object> getExtensions();

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

@JsonIgnore
public abstract boolean getExampleSetFlag();

@JsonInclude(JsonInclude.Include.CUSTOM)
public abstract Object getExample();

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public class SwaggerConfiguration implements OpenAPIConfiguration {
private Set<String> modelConverterClasses;
private String objectMapperProcessorClass;

private Boolean sortOutput;

public Long getCacheTTL() {
return cacheTTL;
}
Expand Down Expand Up @@ -231,4 +233,27 @@ public SwaggerConfiguration modelConverterClasses(Set<String> modelConverterClas
this.modelConverterClasses = modelConverterClasses;
return this;
}

/**
* @since 2.1.6
*/
@Override
public Boolean isSortOutput() {
return sortOutput;
}

/**
* @since 2.1.6
*/
public void setSortOutput(Boolean sortOutput) {
this.sortOutput = sortOutput;
}

/**
* @since 2.1.6
*/
public SwaggerConfiguration sortOutput(Boolean sortOutput) {
setSortOutput(sortOutput);
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,24 @@
*/
public interface ObjectMapperProcessor {

void processJsonObjectMapper(ObjectMapper mapper);
default void processJsonObjectMapper(ObjectMapper mapper) {};

/**
* @deprecated since 2.0.7, as no-op
*
*/
@Deprecated
void processYamlObjectMapper(ObjectMapper mapper);
default void processYamlObjectMapper(ObjectMapper mapper) {}

/**
* @since 2.1.6
*/
default void processOutputJsonObjectMapper(ObjectMapper mapper) {}

/**
* @since 2.1.6
*/
default void processOutputYamlObjectMapper(ObjectMapper mapper) {
processOutputJsonObjectMapper(mapper);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,9 @@ public interface OpenAPIConfiguration {
*/
public Set<String> getModelConverterClasses();

/**
* @since 2.1.6
*/
Boolean isSortOutput();

}
Loading

0 comments on commit 2907229

Please sign in to comment.