Skip to content

Commit

Permalink
Merge pull request #4 from funkyFangs/SC-3-update-annotations-to-be-c…
Browse files Browse the repository at this point in the history
…ompile-only

SC-3: update annotations to be compile only
  • Loading branch information
funkyFangs authored May 23, 2023
2 parents 4a6941f + d36420c commit 7debf95
Show file tree
Hide file tree
Showing 16 changed files with 329 additions and 331 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

import io.swagger.v3.oas.models.OpenAPI;

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

/**
* An annotation for examples which should be injected into the {@link OpenAPI} specification.
*
* @author Harper Price
* @since 2.1.0
*/
// TODO: Investigate @Reflective annotation
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Retention(RetentionPolicy.SOURCE)
public @interface ExampleDetails {
/**
* The name of an example.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* @since 2.1.0
*/
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Retention(RetentionPolicy.SOURCE)
public @interface ExampleMethod {
/**
* The name of the method to target. This method must have an associated HTTP mapping
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* @since 2.1.0
*/
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Retention(RetentionPolicy.SOURCE)
public @interface ExampleTarget {
/**
* The {@link Controller} to target.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* @since 2.1.0
*/
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Retention(RetentionPolicy.SOURCE)
public @interface ExampleType {
/**
* The type of the example.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,30 @@

import com.google.common.annotations.VisibleForTesting;
import io.funky.fangs.springdoc.customizer.annotation.ExampleDetails;
import io.funky.fangs.springdoc.customizer.annotation.ExampleTarget;
import io.funky.fangs.springdoc.customizer.annotation.ExampleType;
import io.funky.fangs.springdoc.customizer.model.ExampleDetailsRecord;
import io.funky.fangs.springdoc.customizer.model.ExampleTargetRecord;
import io.funky.fangs.springdoc.customizer.model.ExampleTypeRecord;
import io.funky.fangs.springdoc.customizer.processor.ExamplesProcessor;
import io.funky.fangs.springdoc.customizer.utility.ExampleUtilities;
import io.funky.fangs.springdoc.customizer.utility.RequestMappingUtilities;
import io.funky.fangs.springdoc.customizer.utility.ReflectionUtilities;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.media.Content;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import jakarta.validation.Validator;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springframework.web.bind.annotation.RequestMethod;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;

import static io.funky.fangs.springdoc.customizer.utility.ExampleUtilities.*;
import static io.funky.fangs.springdoc.customizer.utility.ReflectionUtilities.*;
import static io.funky.fangs.springdoc.customizer.utility.RequestMappingUtilities.*;
import static java.util.Collections.emptyList;
import static lombok.AccessLevel.PACKAGE;

/**
Expand All @@ -39,27 +39,21 @@
@VisibleForTesting
@Getter(PACKAGE)
public class ExamplesOpenApiCustomizer implements OpenApiCustomizer {
@Nonnull
private final String group;

@Nullable
private final String defaultConsumesMediaType;

@Nullable
private final String defaultProducesMediaType;

@Nullable
private final Validator validator;
private final Map<Field, ExampleDetailsRecord> exampleFields = ExamplesProcessor.getExampleFields();

/**
* @param group the group of a grouped REST API, or null
* @param validator the {@link Validator} used to validate examples, or null
*/
public ExamplesOpenApiCustomizer(@Nullable String group,
@Nullable String defaultConsumesMediaType,
@Nullable String defaultProducesMediaType,
@Nullable Validator validator) {
this.group = group == null ? "" : group;
public ExamplesOpenApiCustomizer(String group,
String defaultConsumesMediaType,
String defaultProducesMediaType,
Validator validator) {
this.group = Optional.ofNullable(group).orElse("");
this.defaultConsumesMediaType = defaultConsumesMediaType;
this.defaultProducesMediaType = defaultProducesMediaType;
this.validator = validator;
Expand All @@ -70,11 +64,11 @@ public void customise(OpenAPI openApi) {
var paths = openApi.getPaths();

if (paths != null)
for (var field : ExampleUtilities.getExampleFields()) {
var exampleDetails = field.getAnnotation(ExampleDetails.class);
var value = getFieldValue(field);
for (var entry : exampleFields.entrySet()) {
var value = ReflectionUtilities.getFieldValue(entry.getKey());

if (isValid(value)) {
var exampleDetails = entry.getValue();
var example = new NamedExample()
.name(exampleDetails.name())
.summary(exampleDetails.summary())
Expand All @@ -92,43 +86,42 @@ private boolean isValid(Object value) {
return value != null && (validator == null || validator.validate(value).isEmpty());
}

private void injectExample(NamedExample example, ExampleTarget target, Paths specificationPaths) {
private void injectExample(NamedExample example, ExampleTargetRecord target, Paths specificationPaths) {
var exampleClass = example.getValue().getClass();
var controller = target.controller();
var controllerPaths = getRequestMappingPaths(controller)
var controller = ReflectionUtilities.getClassSafely(target.controller());
var controllerPaths = ExampleUtilities.getRequestMappingPaths(controller)
.stream()
.map(group::concat)
.distinct()
.toList();

var exampleMethods = getControllerMethods(controller, target.methods());
var exampleMethods = ExampleUtilities.getControllerMethods(controller, exampleClass, target.methods());

for (var exampleMethod : exampleMethods.keySet()) {
var methodRequestMappings = exampleMethods.get(exampleMethod)
for (var entry : exampleMethods.entrySet()) {
var methodRequestMappings = Optional.ofNullable(entry.getValue())
.orElse(emptyList())
.stream()
.filter(method -> isRequestExample(exampleMethod) && hasRequestParameter(method, exampleClass)
|| isResponseExample(exampleMethod) && hasResponseType(method, exampleClass))
.map(RequestMappingUtilities::getRequestMapping)
.map(ExampleUtilities::getRequestMapping)
.toList();

for (var methodRequestMapping : methodRequestMappings) {
var paths = getRequestMappingPaths(methodRequestMapping).stream()
var paths = ExampleUtilities.getRequestMappingPaths(methodRequestMapping).stream()
.flatMap(methodPath -> controllerPaths.stream()
.map(controllerPath -> controllerPath + methodPath))
.distinct()
.map(RequestMappingUtilities::normalizePath)
.map(ExampleUtilities::normalizePath)
.toList();

paths.stream()
.map(specificationPaths::get)
.filter(Objects::nonNull)
.forEach(pathItem -> injectIntoPath(example, pathItem, exampleMethod.types(),
.forEach(pathItem -> injectIntoPath(example, pathItem, entry.getKey().types(),
methodRequestMapping.method()));
}
}
}

private void injectIntoPath(NamedExample example, PathItem pathItem, ExampleType[] exampleTypes,
private void injectIntoPath(NamedExample example, PathItem pathItem, Collection<ExampleTypeRecord> exampleTypes,
RequestMethod[] requestMethods) {
var operationsMap = pathItem.readOperationsMap();

Expand All @@ -141,13 +134,14 @@ private void injectIntoPath(NamedExample example, PathItem pathItem, ExampleType

for (var exampleType : exampleTypes)
operations.stream()
.map(operation -> getContents(exampleType, operation))
.map(operation -> ExampleUtilities.getContents(exampleType, operation))
.flatMap(Collection::stream)
.forEach(content -> injectIntoContent(example, content, exampleType));
}

private void injectIntoContent(NamedExample example, Content content, ExampleType exampleType) {
for (var mediaTypeValue : getMediaTypes(exampleType, defaultConsumesMediaType, defaultProducesMediaType)) {
private void injectIntoContent(NamedExample example, Content content, ExampleTypeRecord exampleType) {
for (var mediaTypeValue : ExampleUtilities.getMediaTypes(exampleType, defaultConsumesMediaType,
defaultProducesMediaType)) {
Optional.ofNullable(content.get(mediaTypeValue))
.ifPresent(mediaType -> mediaType.addExamples(example.getName(), example));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ public class SpringDocCustomizerConfigurationProperties {
public static final String PREFIX = Constants.SPRINGDOC_PREFIX + ".customizer";

@NestedConfigurationProperty
private ExamplesCustomizerConfigurationProperties examples;
private ExamplesCustomizerConfigurationProperties examples = new ExamplesCustomizerConfigurationProperties();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.funky.fangs.springdoc.customizer.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import io.funky.fangs.springdoc.customizer.annotation.ExampleDetails;

import java.util.Collection;
import java.util.List;
import java.util.Optional;

import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY;
import static java.util.Collections.emptyList;

/**
*
* @param name
* @param summary
* @param description
* @param targets
* @see ExampleDetails
*/
public record ExampleDetailsRecord(String name, String summary, String description,
@JsonInclude(NON_EMPTY) Collection<ExampleTargetRecord> targets) {
public ExampleDetailsRecord(String name, String summary, String description,
Collection<ExampleTargetRecord> targets) {
this.name = name;
this.summary = summary;
this.description = description;
this.targets = Optional.ofNullable(targets)
.map(List::copyOf)
.orElse(emptyList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.funky.fangs.springdoc.customizer.model;

import com.fasterxml.jackson.annotation.JsonInclude;

import java.util.Collection;
import java.util.List;
import java.util.Optional;

import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY;
import static java.util.Collections.emptyList;

public record ExampleMethodRecord(String name, @JsonInclude(NON_EMPTY) Collection<ExampleTypeRecord> types) {
public ExampleMethodRecord(String name, Collection<ExampleTypeRecord> types) {
this.name = name;
this.types = Optional.ofNullable(types)
.map(List::copyOf)
.orElse(emptyList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.funky.fangs.springdoc.customizer.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import io.funky.fangs.springdoc.customizer.annotation.ExampleTarget;

import java.util.Collection;
import java.util.List;
import java.util.Optional;

import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY;
import static java.util.Collections.emptyList;

/**
* @param controller
* @param methods
* @see ExampleTarget
*/
public record ExampleTargetRecord(String controller, @JsonInclude(NON_EMPTY) Collection<ExampleMethodRecord> methods) {
public ExampleTargetRecord(String controller, Collection<ExampleMethodRecord> methods) {
this.controller = controller;
this.methods = Optional.ofNullable(methods)
.map(List::copyOf)
.orElse(emptyList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.funky.fangs.springdoc.customizer.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import io.funky.fangs.springdoc.customizer.annotation.ExampleType.Type;
import org.springframework.http.HttpStatus;

import java.util.Collection;
import java.util.EnumSet;
import java.util.Optional;
import java.util.Set;

import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY;
import static java.util.Collections.emptySet;
import static java.util.Collections.unmodifiableSet;
import static java.util.function.Predicate.not;

public record ExampleTypeRecord(Type type, @JsonInclude(NON_EMPTY) Collection<String> mediaTypes,
@JsonInclude(NON_EMPTY) Collection<HttpStatus> responses) {
public ExampleTypeRecord(Type type, Collection<String> mediaTypes, Collection<HttpStatus> responses) {
this.type = type;
this.mediaTypes = Optional.ofNullable(mediaTypes)
.map(Set::copyOf)
.orElse(emptySet());
this.responses = Optional.ofNullable(responses)
.filter(not(Collection::isEmpty))
.map(collection -> unmodifiableSet(EnumSet.copyOf(collection)))
.orElse(unmodifiableSet(EnumSet.noneOf(HttpStatus.class)));
}
}
Loading

0 comments on commit 7debf95

Please sign in to comment.